mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: finish the web-endpoint plugin, add ctx.desktop (B8)
This commit is contained in:
1 parent
1682861853
commit
535126fd1b
50 files changed
+2018
-898
No files matched your search
@@ -13,7 +13,7 @@ import {
|
||||
createCurrentPluginSettingsRepository,
|
||||
} from "../repositories/factory.js";
|
||||
import { validateParentHostId } from "./host-parent-validation.js";
|
||||
import { serializeWebUiConfig } from "./host-web-endpoints.js";
|
||||
import { applyPluginHostImportSettings } from "./host-plugin-settings.js";
|
||||
import {
|
||||
isNonEmptyString,
|
||||
isValidPort,
|
||||
@@ -286,10 +286,6 @@ export function registerHostBulkRoutes(
|
||||
simpleUpdates.enableFileManager = updates.enableFileManager;
|
||||
if (typeof updates.enableDocker === "boolean")
|
||||
simpleUpdates.enableDocker = updates.enableDocker;
|
||||
if (typeof updates.enableWebUi === "boolean") {
|
||||
simpleUpdates.enableWebUi = updates.enableWebUi;
|
||||
if (!updates.enableWebUi) simpleUpdates.webUiConfig = null;
|
||||
}
|
||||
if (typeof updates.enableTmuxMonitor === "boolean")
|
||||
simpleUpdates.enableTmuxMonitor = updates.enableTmuxMonitor;
|
||||
if (typeof updates.enableTerminalToolbar === "boolean")
|
||||
@@ -377,6 +373,39 @@ export function registerHostBulkRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
// Web endpoint enable/disable lives in the plugin's own host-scope
|
||||
// settings. Disabling clears webUiConfig, matching the old column
|
||||
// write it replaces.
|
||||
if (typeof updates.enableWebUi === "boolean") {
|
||||
const pluginSettingsRepository =
|
||||
createCurrentPluginSettingsRepository();
|
||||
for (const host of ownedHosts) {
|
||||
try {
|
||||
const scopeId = String(host.id);
|
||||
await pluginSettingsRepository.set(
|
||||
"web-endpoint",
|
||||
"host",
|
||||
scopeId,
|
||||
"enableWebUi",
|
||||
JSON.stringify(updates.enableWebUi),
|
||||
);
|
||||
if (!updates.enableWebUi) {
|
||||
await pluginSettingsRepository.set(
|
||||
"web-endpoint",
|
||||
"host",
|
||||
scopeId,
|
||||
"webUiConfig",
|
||||
JSON.stringify(null),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
errors.push(
|
||||
`Failed to ${updates.enableWebUi ? "enable" : "disable"} web endpoints for host ${host.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
updated: ownedIds.length,
|
||||
failed: unauthorizedIds.length,
|
||||
@@ -737,7 +766,6 @@ export function registerHostBulkRoutes(
|
||||
enableTunnel: hostData.enableTunnel !== false,
|
||||
enableFileManager: hostData.enableFileManager !== false,
|
||||
enableDocker: hostData.enableDocker || false,
|
||||
enableWebUi: hostData.enableWebUi || false,
|
||||
enableTmuxMonitor: hostData.enableTmuxMonitor || false,
|
||||
enableTerminalToolbar: hostData.enableTerminalToolbar !== false,
|
||||
enableAiAssistant: hostData.enableAiAssistant || false,
|
||||
@@ -762,9 +790,6 @@ export function registerHostBulkRoutes(
|
||||
dockerConfig: hostData.dockerConfig
|
||||
? JSON.stringify(hostData.dockerConfig)
|
||||
: null,
|
||||
webUiConfig: hostData.enableWebUi
|
||||
? serializeWebUiConfig(hostData.webUiConfig)
|
||||
: null,
|
||||
terminalConfig: hostData.terminalConfig
|
||||
? JSON.stringify(hostData.terminalConfig)
|
||||
: null,
|
||||
@@ -860,27 +885,14 @@ export function registerHostBulkRoutes(
|
||||
results.success++;
|
||||
}
|
||||
|
||||
if (hostData.enableProxmox || hostData.proxmoxConfig) {
|
||||
const pluginSettingsRepository =
|
||||
createCurrentPluginSettingsRepository();
|
||||
const scopeId = String(savedHostId);
|
||||
await pluginSettingsRepository.set(
|
||||
"proxmox",
|
||||
"host",
|
||||
scopeId,
|
||||
"enableProxmox",
|
||||
JSON.stringify(hostData.enableProxmox || false),
|
||||
);
|
||||
if (hostData.proxmoxConfig) {
|
||||
await pluginSettingsRepository.set(
|
||||
"proxmox",
|
||||
"host",
|
||||
scopeId,
|
||||
"proxmoxConfig",
|
||||
JSON.stringify(hostData.proxmoxConfig),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Every enabled plugin that declares host-scope settings and
|
||||
// registered a hostImportNormalizer validates and writes its own
|
||||
// fields here, so this loop does not need to know which plugins
|
||||
// exist. See host-plugin-settings.ts.
|
||||
await applyPluginHostImportSettings(
|
||||
savedHostId,
|
||||
hostData as Record<string, unknown>,
|
||||
);
|
||||
} catch (error) {
|
||||
results.failed++;
|
||||
results.errors.push(`Host ${i + 1}: ${getErrorMessage(error)}`);
|
||||
@@ -1038,7 +1050,6 @@ export function registerHostBulkRoutes(
|
||||
enableTunnel: true,
|
||||
enableFileManager: true,
|
||||
enableDocker: false,
|
||||
enableWebUi: false,
|
||||
enableTmuxMonitor: false,
|
||||
enableTerminalToolbar: true,
|
||||
enableAiAssistant: false,
|
||||
@@ -1056,7 +1067,6 @@ export function registerHostBulkRoutes(
|
||||
quickActions: null,
|
||||
statsConfig: null,
|
||||
dockerConfig: null,
|
||||
webUiConfig: null,
|
||||
terminalConfig: null,
|
||||
forceKeyboardInteractive: "false",
|
||||
notes: null,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AuthOverrideProtocol } from "../../../types/auth-protocols.js";
|
||||
import { parseWebUiConfig } from "./host-web-endpoints.js";
|
||||
|
||||
export function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
@@ -364,8 +363,6 @@ const CONNECT_LEVEL_FIELDS = new Set([
|
||||
"enableTunnel",
|
||||
"enableFileManager",
|
||||
"enableDocker",
|
||||
"enableWebUi",
|
||||
"webUiConfig",
|
||||
"enableTmuxMonitor",
|
||||
"enableTerminalToolbar",
|
||||
"enableAiAssistant",
|
||||
@@ -467,7 +464,6 @@ export function transformHostResponse(
|
||||
enableTunnel: !!host.enableTunnel,
|
||||
enableFileManager: host.enableFileManager !== false,
|
||||
enableDocker: !!host.enableDocker,
|
||||
enableWebUi: !!host.enableWebUi,
|
||||
enableTmuxMonitor: !!host.enableTmuxMonitor,
|
||||
enableTerminalToolbar: host.enableTerminalToolbar !== false,
|
||||
enableAiAssistant: !!host.enableAiAssistant,
|
||||
@@ -519,9 +515,6 @@ export function transformHostResponse(
|
||||
dockerConfig: host.dockerConfig
|
||||
? JSON.parse(host.dockerConfig as string)
|
||||
: undefined,
|
||||
// Guarded, unlike dockerConfig directly above: parseWebUiConfig never
|
||||
// throws, so a half-written config cannot take out the whole host listing.
|
||||
webUiConfig: parseWebUiConfig(host.webUiConfig),
|
||||
forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
|
||||
useWarpgate: !!host.useWarpgate,
|
||||
socks5ProxyChain: host.socks5ProxyChain
|
||||
|
||||
@@ -15,12 +15,13 @@ import { createCurrentPluginSettingsRepository } from "../repositories/factory.j
|
||||
import type { PluginSettingsRecord } from "../repositories/plugin-settings-repository.js";
|
||||
import { declaredFields, resolveFieldValue } from "../../plugins/settings.js";
|
||||
import { getPluginRuntime } from "../../plugins/index.js";
|
||||
import { consume } from "../../plugins/registry.js";
|
||||
import { sshLogger } from "../../utils/logger.js";
|
||||
|
||||
export type HostPluginSettings = Record<string, Record<string, unknown>>;
|
||||
|
||||
/** Enabled plugins that declare host-scope settings, with their manifests. */
|
||||
function hostSettingsPlugins(): PluginManifest[] {
|
||||
export function hostSettingsPlugins(): PluginManifest[] {
|
||||
try {
|
||||
const { loader } = getPluginRuntime();
|
||||
return loader
|
||||
@@ -147,3 +148,48 @@ export async function writeHostPluginSettings(
|
||||
await repository.set(pluginId, "host", scopeId, key, JSON.stringify(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A plugin's own validator for the fields it accepts inline on a bulk host
|
||||
* import row (host-bulk-routes.ts's Termix-JSON import path). Registered
|
||||
* through ctx.registry.provide("<pluginId>.hostImportNormalizer", fn) - not
|
||||
* part of the SDK's typed ctx surface, because only this one bulk-import
|
||||
* path calls it and a full contract is speculative until a second caller
|
||||
* needs it. Returns the fields to write (JSON-serializable, matching
|
||||
* writeHostPluginSettings's input), or null to write nothing for this row.
|
||||
*/
|
||||
export type PluginHostImportNormalizer = (
|
||||
raw: Record<string, unknown>,
|
||||
) => Record<string, unknown> | null;
|
||||
|
||||
/**
|
||||
* Runs every enabled plugin's registered import normalizer over one imported
|
||||
* host row and writes whatever each one returns, so a plugin's host-scope
|
||||
* settings are validated the same way on bulk import as they are anywhere
|
||||
* else - instead of host-bulk-routes.ts hardcoding one plugin's shape.
|
||||
*
|
||||
* Best-effort per plugin: one plugin's normalizer throwing must not fail the
|
||||
* whole import, so it is logged and skipped rather than propagated.
|
||||
*/
|
||||
export async function applyPluginHostImportSettings(
|
||||
hostId: number,
|
||||
raw: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
for (const manifest of hostSettingsPlugins()) {
|
||||
const normalizer = consume<PluginHostImportNormalizer>(
|
||||
`${manifest.id}.hostImportNormalizer`,
|
||||
);
|
||||
if (!normalizer) continue;
|
||||
try {
|
||||
const values = normalizer(raw);
|
||||
if (values) await writeHostPluginSettings(manifest.id, hostId, values);
|
||||
} catch (error) {
|
||||
sshLogger.warn("Plugin host import normalizer failed", {
|
||||
operation: "host_import_plugin_settings",
|
||||
pluginId: manifest.id,
|
||||
hostId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import {
|
||||
MAX_WEB_ENDPOINTS,
|
||||
MAX_WEB_ENDPOINT_LABEL_LENGTH,
|
||||
} from "../../../types/index.js";
|
||||
import type { WebEndpoint, WebUiConfig } from "../../../types/index.js";
|
||||
|
||||
export { MAX_WEB_ENDPOINTS, MAX_WEB_ENDPOINT_LABEL_LENGTH };
|
||||
|
||||
const SCHEMES = new Set(["http", "https"]);
|
||||
const ACCESS_VALUES = new Set(["direct", "tunnel"]);
|
||||
const RENDER_VALUES = new Set(["external", "embedded"]);
|
||||
|
||||
const MIN_PORT = 1;
|
||||
const MAX_PORT = 65535;
|
||||
|
||||
function isValidPort(value: unknown): value is number {
|
||||
return (
|
||||
typeof value === "number" &&
|
||||
Number.isInteger(value) &&
|
||||
value >= MIN_PORT &&
|
||||
value <= MAX_PORT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* C0 controls and DEL. Checked by codepoint rather than by regex character
|
||||
* class so no escaping subtlety can let one through: a path like
|
||||
* "\t//evil.example" defeats a naive startsWith("//") guard, because the
|
||||
* browser strips the control character and then follows the authority.
|
||||
*/
|
||||
function hasControlCharacter(value: string): boolean {
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const code = value.charCodeAt(index);
|
||||
if (code <= 0x1f || code === 0x7f) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces a path to begin with "/", or returns null to drop the endpoint.
|
||||
*
|
||||
* This is THE enforcement point for a value that reaches an href and an
|
||||
* iframe src -- the editor's checks are UX. Anything that could redirect the
|
||||
* frame away from the endpoint is rejected rather than sanitized, because
|
||||
* sanitizing invites a second, subtly different implementation elsewhere.
|
||||
*/
|
||||
function normalizePath(raw: unknown): string | null {
|
||||
if (raw === undefined || raw === null || raw === "") return "/";
|
||||
if (typeof raw !== "string") return null;
|
||||
if (hasControlCharacter(raw)) return null;
|
||||
|
||||
const path = raw.startsWith("/") ? raw : `/${raw}`;
|
||||
|
||||
if (path.startsWith("//")) return null;
|
||||
if (/^\/[a-z][a-z0-9+.-]*:/i.test(path)) return null;
|
||||
if (path.includes("\\")) return null;
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bare host literal, or null to drop the endpoint, or undefined to leave the
|
||||
* field unset (the route then falls back to loopback).
|
||||
*
|
||||
* The value lands both in a TCP listener and in a URL authority, so nothing
|
||||
* carrying a scheme, port, path or credentials may survive.
|
||||
*/
|
||||
function normalizeBindHost(raw: unknown): string | null | undefined {
|
||||
if (raw === undefined || raw === null || raw === "") return undefined;
|
||||
if (typeof raw !== "string") return null;
|
||||
|
||||
const value = raw.trim();
|
||||
if (!value) return undefined;
|
||||
if (value.length > 253) return null;
|
||||
if (hasControlCharacter(value)) return null;
|
||||
|
||||
const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
||||
const HOSTNAME =
|
||||
/^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/;
|
||||
const IPV6_BRACKETED = /^\[[0-9A-Fa-f:.]+\]$/;
|
||||
|
||||
if (IPV4.test(value)) {
|
||||
return value.split(".").every((part) => Number(part) <= 255) ? value : null;
|
||||
}
|
||||
if (IPV6_BRACKETED.test(value) || HOSTNAME.test(value)) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeEndpoint(raw: unknown): WebEndpoint | null {
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const input = raw as Record<string, unknown>;
|
||||
|
||||
const id = typeof input.id === "string" ? input.id.trim() : "";
|
||||
if (!id) return null;
|
||||
|
||||
const label = typeof input.label === "string" ? input.label.trim() : "";
|
||||
if (!label) return null;
|
||||
|
||||
const scheme = input.scheme;
|
||||
if (typeof scheme !== "string" || !SCHEMES.has(scheme)) return null;
|
||||
|
||||
if (!isValidPort(input.port)) return null;
|
||||
|
||||
const path = normalizePath(input.path);
|
||||
if (path === null) return null;
|
||||
|
||||
const access = input.access;
|
||||
if (typeof access !== "string" || !ACCESS_VALUES.has(access)) return null;
|
||||
|
||||
const render = input.render;
|
||||
if (typeof render !== "string" || !RENDER_VALUES.has(render)) return null;
|
||||
|
||||
const isTunnel = access === "tunnel";
|
||||
|
||||
const endpoint: WebEndpoint = {
|
||||
id,
|
||||
label: label.slice(0, MAX_WEB_ENDPOINT_LABEL_LENGTH),
|
||||
scheme: scheme as WebEndpoint["scheme"],
|
||||
port: input.port,
|
||||
path,
|
||||
access: access as WebEndpoint["access"],
|
||||
render: render as WebEndpoint["render"],
|
||||
// Meaningless for a tunnel, whose host component is loopback and already
|
||||
// exempt from certificate checks. Cleared rather than carried so nothing
|
||||
// downstream has to re-derive that.
|
||||
ignoreCert: !isTunnel && input.ignoreCert === true,
|
||||
};
|
||||
|
||||
// Both are tunnel-only. Silently ignored on a direct endpoint rather than
|
||||
// rejected: they describe a forward that a direct endpoint never creates.
|
||||
if (isTunnel) {
|
||||
const bindHost = normalizeBindHost(input.bindHost);
|
||||
if (bindHost === null) return null;
|
||||
if (bindHost !== undefined) endpoint.bindHost = bindHost;
|
||||
|
||||
if (input.localPort !== undefined && input.localPort !== null) {
|
||||
if (!isValidPort(input.localPort)) return null;
|
||||
endpoint.localPort = input.localPort;
|
||||
}
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops any endpoint it refuses rather than rejecting the whole host -- one
|
||||
* bad row must not make a host unsaveable or unlistable. The editor is
|
||||
* responsible for telling the user before that happens
|
||||
* (plugins/web-endpoint/src/frontend/web-endpoint-validation.ts).
|
||||
*/
|
||||
export function normalizeWebEndpoints(raw: unknown): WebEndpoint[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
||||
const seen = new Set<string>();
|
||||
const endpoints: WebEndpoint[] = [];
|
||||
|
||||
for (const candidate of raw) {
|
||||
if (endpoints.length >= MAX_WEB_ENDPOINTS) break;
|
||||
const endpoint = normalizeEndpoint(candidate);
|
||||
if (!endpoint) continue;
|
||||
if (seen.has(endpoint.id)) continue;
|
||||
seen.add(endpoint.id);
|
||||
endpoints.push(endpoint);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Never throws. A malformed stored value yields an empty endpoint list, so a
|
||||
* half-written config cannot take out the whole host listing -- which is what
|
||||
* dockerConfig's bare JSON.parse does today.
|
||||
*/
|
||||
export function parseWebUiConfig(raw: unknown): WebUiConfig {
|
||||
if (raw === undefined || raw === null) return { endpoints: [] };
|
||||
|
||||
let value: unknown = raw;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
value = JSON.parse(raw);
|
||||
} catch {
|
||||
return { endpoints: [] };
|
||||
}
|
||||
}
|
||||
|
||||
if (!value || typeof value !== "object") return { endpoints: [] };
|
||||
return {
|
||||
endpoints: normalizeWebEndpoints(
|
||||
(value as { endpoints?: unknown }).endpoints,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** Null when nothing survives normalization, so the column is cleared. */
|
||||
export function serializeWebUiConfig(config: unknown): string | null {
|
||||
const endpoints = normalizeWebEndpoints(
|
||||
(config as { endpoints?: unknown } | null | undefined)?.endpoints,
|
||||
);
|
||||
if (endpoints.length === 0) return null;
|
||||
return JSON.stringify({ endpoints });
|
||||
}
|
||||
@@ -72,10 +72,6 @@ import type {
|
||||
HostResolutionHostRecord,
|
||||
} from "../repositories/host-resolution-repository.js";
|
||||
import { AUTH_PROTOCOL_METADATA } from "../../../types/auth-protocols.js";
|
||||
import {
|
||||
parseWebUiConfig,
|
||||
serializeWebUiConfig,
|
||||
} from "./host-web-endpoints.js";
|
||||
import {
|
||||
requiresPersonalHostAuthentication,
|
||||
resolveRecipientSharedHostAuthentication,
|
||||
@@ -202,7 +198,6 @@ router.post(
|
||||
enableFileManager,
|
||||
scpLegacy,
|
||||
enableDocker,
|
||||
enableWebUi,
|
||||
enableProxmox,
|
||||
enableTmuxMonitor,
|
||||
enableTerminalToolbar,
|
||||
@@ -219,7 +214,6 @@ router.post(
|
||||
quickActions,
|
||||
statsConfig,
|
||||
dockerConfig,
|
||||
webUiConfig,
|
||||
proxmoxConfig,
|
||||
enableProxmoxStats,
|
||||
proxmoxStatsConfig,
|
||||
@@ -348,7 +342,6 @@ router.post(
|
||||
enableFileManager: enableFileManager ? 1 : 0,
|
||||
scpLegacy: scpLegacy ? 1 : 0,
|
||||
enableDocker: enableDocker ? 1 : 0,
|
||||
enableWebUi: enableWebUi ? 1 : 0,
|
||||
enableTmuxMonitor: enableTmuxMonitor ? 1 : 0,
|
||||
enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1,
|
||||
enableAiAssistant: enableAiAssistant ? 1 : 0,
|
||||
@@ -369,13 +362,6 @@ router.post(
|
||||
? dockerConfig
|
||||
: JSON.stringify(dockerConfig)
|
||||
: null,
|
||||
webUiConfig: enableWebUi
|
||||
? serializeWebUiConfig(
|
||||
typeof webUiConfig === "string"
|
||||
? safeParseJson(webUiConfig)
|
||||
: webUiConfig,
|
||||
)
|
||||
: null,
|
||||
terminalConfig: terminalConfig
|
||||
? typeof terminalConfig === "string"
|
||||
? terminalConfig
|
||||
@@ -930,7 +916,6 @@ router.put(
|
||||
enableFileManager,
|
||||
scpLegacy,
|
||||
enableDocker,
|
||||
enableWebUi,
|
||||
enableProxmox,
|
||||
enableTmuxMonitor,
|
||||
enableTerminalToolbar,
|
||||
@@ -947,7 +932,6 @@ router.put(
|
||||
quickActions,
|
||||
statsConfig,
|
||||
dockerConfig,
|
||||
webUiConfig,
|
||||
proxmoxConfig,
|
||||
enableProxmoxStats,
|
||||
proxmoxStatsConfig,
|
||||
@@ -1079,7 +1063,6 @@ router.put(
|
||||
enableFileManager: enableFileManager ? 1 : 0,
|
||||
scpLegacy: scpLegacy ? 1 : 0,
|
||||
enableDocker: enableDocker ? 1 : 0,
|
||||
enableWebUi: enableWebUi ? 1 : 0,
|
||||
enableTmuxMonitor: enableTmuxMonitor ? 1 : 0,
|
||||
enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1,
|
||||
enableAiAssistant: enableAiAssistant ? 1 : 0,
|
||||
@@ -1100,13 +1083,6 @@ router.put(
|
||||
? dockerConfig
|
||||
: JSON.stringify(dockerConfig)
|
||||
: null,
|
||||
webUiConfig: enableWebUi
|
||||
? serializeWebUiConfig(
|
||||
typeof webUiConfig === "string"
|
||||
? safeParseJson(webUiConfig)
|
||||
: webUiConfig,
|
||||
)
|
||||
: null,
|
||||
terminalConfig: terminalConfig
|
||||
? typeof terminalConfig === "string"
|
||||
? terminalConfig
|
||||
@@ -2100,9 +2076,10 @@ router.get(
|
||||
}
|
||||
|
||||
const resolvedHost = (await resolveHostCredentials(host, userId)) || host;
|
||||
const proxmoxSettings = (
|
||||
const hostPluginSettings = (
|
||||
await loadHostPluginSettings([Number(hostId)])
|
||||
).get(Number(hostId))?.proxmox as
|
||||
).get(Number(hostId));
|
||||
const proxmoxSettings = hostPluginSettings?.proxmox as
|
||||
| {
|
||||
enableProxmox?: boolean;
|
||||
proxmoxConfig?: unknown;
|
||||
@@ -2110,6 +2087,8 @@ router.get(
|
||||
proxmoxStatsConfig?: unknown;
|
||||
}
|
||||
| undefined;
|
||||
const webEndpointSettings = hostPluginSettings?.["web-endpoint"] as
|
||||
{ enableWebUi?: boolean; webUiConfig?: unknown } | undefined;
|
||||
|
||||
const exportedConnectionType =
|
||||
(resolvedHost.connectionType as string) || "ssh";
|
||||
@@ -2177,7 +2156,7 @@ router.get(
|
||||
enableFileManager: resolvedHost.enableFileManager !== false,
|
||||
scpLegacy: !!resolvedHost.scpLegacy,
|
||||
enableDocker: !!resolvedHost.enableDocker,
|
||||
enableWebUi: !!resolvedHost.enableWebUi,
|
||||
enableWebUi: !!webEndpointSettings?.enableWebUi,
|
||||
enableProxmox: !!proxmoxSettings?.enableProxmox,
|
||||
enableProxmoxStats: !!proxmoxSettings?.enableProxmoxStats,
|
||||
enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor,
|
||||
@@ -2202,7 +2181,7 @@ router.get(
|
||||
statsConfig: resolvedHost.statsConfig
|
||||
? JSON.parse(resolvedHost.statsConfig as string)
|
||||
: null,
|
||||
webUiConfig: parseWebUiConfig(resolvedHost.webUiConfig),
|
||||
webUiConfig: webEndpointSettings?.webUiConfig ?? { endpoints: [] },
|
||||
dockerConfig: resolvedHost.dockerConfig
|
||||
? JSON.parse(resolvedHost.dockerConfig as string)
|
||||
: null,
|
||||
@@ -2282,7 +2261,7 @@ router.get(
|
||||
try {
|
||||
const allHosts =
|
||||
await createCurrentHostResolutionRepository().findHostsByUserId(userId);
|
||||
const proxmoxSettingsByHost = await loadHostPluginSettings(
|
||||
const pluginSettingsByHost = await loadHostPluginSettings(
|
||||
allHosts.map((h) => h.id as number),
|
||||
);
|
||||
|
||||
@@ -2293,9 +2272,11 @@ router.get(
|
||||
const resolvedHost = shareMode
|
||||
? host
|
||||
: (await resolveHostCredentials(host, userId)) || host;
|
||||
const proxmoxSettings = proxmoxSettingsByHost.get(host.id as number)
|
||||
?.proxmox as
|
||||
const hostPluginSettings = pluginSettingsByHost.get(host.id as number);
|
||||
const proxmoxSettings = hostPluginSettings?.proxmox as
|
||||
{ enableProxmox?: boolean; proxmoxConfig?: unknown } | undefined;
|
||||
const webEndpointSettings = hostPluginSettings?.["web-endpoint"] as
|
||||
{ enableWebUi?: boolean; webUiConfig?: unknown } | undefined;
|
||||
|
||||
const exportedConnectionType =
|
||||
(resolvedHost.connectionType as string) || "ssh";
|
||||
@@ -2344,7 +2325,7 @@ router.get(
|
||||
enableTunnel: !!resolvedHost.enableTunnel,
|
||||
enableFileManager: resolvedHost.enableFileManager !== false,
|
||||
enableDocker: !!resolvedHost.enableDocker,
|
||||
enableWebUi: !!resolvedHost.enableWebUi,
|
||||
enableWebUi: !!webEndpointSettings?.enableWebUi,
|
||||
enableProxmox: !!proxmoxSettings?.enableProxmox,
|
||||
enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor,
|
||||
enableTerminalToolbar:
|
||||
@@ -2371,7 +2352,9 @@ router.get(
|
||||
statsConfig: resolvedHost.statsConfig
|
||||
? JSON.parse(resolvedHost.statsConfig as string)
|
||||
: null,
|
||||
webUiConfig: parseWebUiConfig(resolvedHost.webUiConfig),
|
||||
webUiConfig: webEndpointSettings?.webUiConfig ?? {
|
||||
endpoints: [],
|
||||
},
|
||||
dockerConfig: resolvedHost.dockerConfig
|
||||
? JSON.parse(resolvedHost.dockerConfig as string)
|
||||
: null,
|
||||
@@ -3010,10 +2993,9 @@ registerHostNetworkRoutes(router, {
|
||||
export default router;
|
||||
|
||||
/**
|
||||
* A webUiConfig arriving as a JSON string (an import, or a client that
|
||||
* stringified it) must still reach serializeWebUiConfig as an object.
|
||||
* Malformed input becomes null, which serializes to a cleared column rather
|
||||
* than throwing inside a host save.
|
||||
* A config field arriving as a JSON string (an import, or a client that
|
||||
* stringified it) must still reach storage as an object. Malformed input
|
||||
* becomes null rather than throwing inside a host save.
|
||||
*/
|
||||
function safeParseJson(raw: string): unknown {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user