mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
217 lines
5.7 KiB
TypeScript
217 lines
5.7 KiB
TypeScript
export type FieldGroup =
|
|
| "connection"
|
|
| "notes"
|
|
| "tags"
|
|
| "proxy"
|
|
| "jumpHosts"
|
|
| "quickActions"
|
|
| "featureFlags"
|
|
| "advanced";
|
|
|
|
export interface ExportPayload {
|
|
version?: string;
|
|
exportedAt?: string;
|
|
credentials?: Record<string, unknown>[];
|
|
hosts: Record<string, unknown>[];
|
|
}
|
|
|
|
const FIELD_GROUP_KEYS: Record<FieldGroup, string[]> = {
|
|
connection: [
|
|
"connectionType",
|
|
"name",
|
|
"ip",
|
|
"port",
|
|
"username",
|
|
"folder",
|
|
"domain",
|
|
"security",
|
|
"ignoreCert",
|
|
],
|
|
notes: ["notes"],
|
|
tags: ["tags", "pin"],
|
|
proxy: [
|
|
"useSocks5",
|
|
"socks5Host",
|
|
"socks5Port",
|
|
"socks5Username",
|
|
"socks5ProxyChain",
|
|
],
|
|
jumpHosts: ["jumpHosts"],
|
|
quickActions: ["quickActions"],
|
|
// Every plugin's host settings, enable switches included.
|
|
featureFlags: ["pluginSettings", "forceKeyboardInteractive"],
|
|
advanced: [
|
|
"statusCheckEnabled",
|
|
"statusCheckInterval",
|
|
"terminalConfig",
|
|
"sshOptions",
|
|
],
|
|
};
|
|
|
|
export const SECRET_KEYS = [
|
|
"password",
|
|
"key",
|
|
"keyPassword",
|
|
"sudoPassword",
|
|
"socks5Password",
|
|
];
|
|
|
|
const CREDENTIAL_KEYS = [
|
|
...SECRET_KEYS,
|
|
"authType",
|
|
"keyType",
|
|
"credentialAlias",
|
|
"credentialId",
|
|
"overrideCredentialUsername",
|
|
];
|
|
|
|
const TUPLE_KEYS = ["name", "ip", "port", "username", "connectionType"];
|
|
|
|
const NESTED_SECRETS: { container: string; field: string }[] = [
|
|
{ container: "guacamoleConfig", field: "gateway-password" },
|
|
];
|
|
|
|
const NESTED_SECRET_ARRAYS: { container: string; field: string }[] = [
|
|
{ container: "socks5ProxyChain", field: "password" },
|
|
];
|
|
|
|
function nestedSecret(
|
|
host: Record<string, unknown>,
|
|
container: string,
|
|
field: string,
|
|
): Record<string, unknown> | null {
|
|
const blob = host[container];
|
|
if (!blob || typeof blob !== "object") return null;
|
|
const record = blob as Record<string, unknown>;
|
|
return field in record ? record : null;
|
|
}
|
|
|
|
/** Plugin host settings can carry the same nested secrets as the host. */
|
|
function mapPluginSettings(
|
|
pluginSettings: unknown,
|
|
apply: (values: Record<string, unknown>) => void,
|
|
): unknown {
|
|
if (!pluginSettings || typeof pluginSettings !== "object") {
|
|
return pluginSettings;
|
|
}
|
|
const perPlugin: Record<string, unknown> = {};
|
|
for (const [pluginId, values] of Object.entries(
|
|
pluginSettings as Record<string, unknown>,
|
|
)) {
|
|
const copy = { ...(values as Record<string, unknown>) };
|
|
apply(copy);
|
|
perPlugin[pluginId] = copy;
|
|
}
|
|
return perPlugin;
|
|
}
|
|
|
|
export function hostKey(host: Record<string, unknown>): string {
|
|
return JSON.stringify(TUPLE_KEYS.map((k) => String(host[k] ?? "")));
|
|
}
|
|
|
|
export function buildExportPayload(
|
|
raw: ExportPayload,
|
|
selected: Set<string> | null,
|
|
groups: Set<FieldGroup>,
|
|
withCredentials: boolean,
|
|
): ExportPayload {
|
|
const allowed = new Set<string>([
|
|
"exportId",
|
|
...CREDENTIAL_KEYS,
|
|
...FIELD_GROUP_KEYS.connection,
|
|
]);
|
|
for (const group of groups) {
|
|
for (const key of FIELD_GROUP_KEYS[group]) allowed.add(key);
|
|
}
|
|
|
|
const hosts = (raw.hosts ?? [])
|
|
.filter((host) => selected === null || selected.has(hostKey(host)))
|
|
.map((host) => {
|
|
const shaped: Record<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(host)) {
|
|
if (allowed.has(key)) shaped[key] = value;
|
|
}
|
|
if (!withCredentials) {
|
|
const clearNested = (target: Record<string, unknown>) => {
|
|
for (const { container, field } of NESTED_SECRETS) {
|
|
const record = nestedSecret(target, container, field);
|
|
if (record) target[container] = { ...record, [field]: null };
|
|
}
|
|
};
|
|
clearNested(shaped);
|
|
shaped.pluginSettings = mapPluginSettings(
|
|
shaped.pluginSettings,
|
|
clearNested,
|
|
);
|
|
for (const { container, field } of NESTED_SECRET_ARRAYS) {
|
|
const arr = shaped[container];
|
|
if (Array.isArray(arr)) {
|
|
shaped[container] = arr.map((entry) =>
|
|
entry && typeof entry === "object"
|
|
? { ...(entry as Record<string, unknown>), [field]: null }
|
|
: entry,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return shaped;
|
|
});
|
|
|
|
const result: ExportPayload = { ...raw, hosts };
|
|
|
|
if (raw.credentials) {
|
|
const used = new Set(
|
|
hosts.map((host) => host.credentialAlias).filter(Boolean),
|
|
);
|
|
result.credentials = raw.credentials.filter((entry) =>
|
|
used.has(entry.alias),
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function maskSecrets(payload: ExportPayload): ExportPayload {
|
|
return {
|
|
...payload,
|
|
hosts: payload.hosts.map((host) => {
|
|
const masked = { ...host };
|
|
for (const key of SECRET_KEYS) {
|
|
if (
|
|
masked[key] !== undefined &&
|
|
masked[key] !== null &&
|
|
masked[key] !== ""
|
|
) {
|
|
masked[key] = "<included>";
|
|
}
|
|
}
|
|
const maskNested = (target: Record<string, unknown>) => {
|
|
for (const { container, field } of NESTED_SECRETS) {
|
|
const record = nestedSecret(target, container, field);
|
|
if (record && record[field]) {
|
|
target[container] = { ...record, [field]: "<included>" };
|
|
}
|
|
}
|
|
};
|
|
maskNested(masked);
|
|
masked.pluginSettings = mapPluginSettings(
|
|
masked.pluginSettings,
|
|
maskNested,
|
|
);
|
|
for (const { container, field } of NESTED_SECRET_ARRAYS) {
|
|
const arr = masked[container];
|
|
if (Array.isArray(arr)) {
|
|
masked[container] = arr.map((entry) => {
|
|
if (entry && typeof entry === "object") {
|
|
const record = entry as Record<string, unknown>;
|
|
if (record[field]) return { ...record, [field]: "<included>" };
|
|
}
|
|
return entry;
|
|
});
|
|
}
|
|
}
|
|
return masked;
|
|
}),
|
|
};
|
|
}
|