Include credentials in host export

- Add backend API to export decrypted host data
- Export plaintext passwords and SSH keys for portability
- Require data access authentication for security
- Update warning messages to reflect plaintext export

Fixes #254
This commit is contained in:
ZacharyZcR
2025-10-02 15:04:50 +08:00
parent 02d1d8d1c0
commit 428ec44f69
5 changed files with 122 additions and 40 deletions
+78
View File
@@ -670,6 +670,84 @@ router.get(
}, },
); );
// Route: Export SSH host with decrypted credentials (requires data access)
// GET /ssh/db/host/:id/export
router.get(
"/db/host/:id/export",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const hostId = req.params.id;
const userId = (req as any).userId;
if (!isNonEmptyString(userId) || !hostId) {
return res.status(400).json({ error: "Invalid userId or hostId" });
}
try {
// Fetch decrypted host data using SimpleDBOps
const hosts = await SimpleDBOps.select(
db
.select()
.from(sshData)
.where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId))),
"ssh_data",
userId,
);
if (hosts.length === 0) {
return res.status(404).json({ error: "SSH host not found" });
}
const host = hosts[0];
// Resolve credentials if using credential-based auth
const resolvedHost = (await resolveHostCredentials(host)) || host;
// Format for export (include all fields including sensitive data)
const exportData = {
name: resolvedHost.name,
ip: resolvedHost.ip,
port: resolvedHost.port,
username: resolvedHost.username,
authType: resolvedHost.authType,
password: resolvedHost.password || null,
key: resolvedHost.key || null,
keyPassword: resolvedHost.keyPassword || null,
keyType: resolvedHost.keyType || null,
folder: resolvedHost.folder,
tags:
typeof resolvedHost.tags === "string"
? resolvedHost.tags.split(",").filter(Boolean)
: resolvedHost.tags || [],
pin: !!resolvedHost.pin,
enableTerminal: !!resolvedHost.enableTerminal,
enableTunnel: !!resolvedHost.enableTunnel,
enableFileManager: !!resolvedHost.enableFileManager,
defaultPath: resolvedHost.defaultPath,
tunnelConnections: resolvedHost.tunnelConnections
? JSON.parse(resolvedHost.tunnelConnections)
: [],
};
sshLogger.success("Host exported with decrypted credentials", {
operation: "host_export",
hostId: parseInt(hostId),
userId,
});
res.json(exportData);
} catch (err) {
sshLogger.error("Failed to export SSH host", err, {
operation: "host_export",
hostId: parseInt(hostId),
userId,
});
res.status(500).json({ error: "Failed to export SSH host" });
}
},
);
// Route: Delete SSH host by id (requires JWT) // Route: Delete SSH host by id (requires JWT)
// DELETE /ssh/host/:id // DELETE /ssh/host/:id
router.delete( router.delete(
+2 -1
View File
@@ -564,10 +564,11 @@
"downloadSample": "Download Sample", "downloadSample": "Download Sample",
"formatGuide": "Format Guide", "formatGuide": "Format Guide",
"exportCredentialWarning": "Warning: Host \"{{name}}\" uses credential authentication. The exported file will not include the credential data and will need to be manually reconfigured after import. Do you want to continue?", "exportCredentialWarning": "Warning: Host \"{{name}}\" uses credential authentication. The exported file will not include the credential data and will need to be manually reconfigured after import. Do you want to continue?",
"exportSensitiveDataWarning": "Warning: Host \"{{name}}\" contains sensitive authentication data (password/SSH key). The exported file will not include this data for security reasons. You'll need to reconfigure authentication after import. Do you want to continue?", "exportSensitiveDataWarning": "Warning: Host \"{{name}}\" contains sensitive authentication data (password/SSH key). The exported file will include this data in plaintext. Please keep the file secure and delete it after use. Do you want to continue?",
"uncategorized": "Uncategorized", "uncategorized": "Uncategorized",
"confirmDelete": "Are you sure you want to delete \"{{name}}\" ?", "confirmDelete": "Are you sure you want to delete \"{{name}}\" ?",
"failedToDeleteHost": "Failed to delete host", "failedToDeleteHost": "Failed to delete host",
"failedToExportHost": "Failed to export host. Please ensure you're logged in and have access to the host data.",
"jsonMustContainHosts": "JSON must contain a \"hosts\" array or be an array of hosts", "jsonMustContainHosts": "JSON must contain a \"hosts\" array or be an array of hosts",
"noHostsInJson": "No hosts found in JSON file", "noHostsInJson": "No hosts found in JSON file",
"maxHostsAllowed": "Maximum 100 hosts allowed per import", "maxHostsAllowed": "Maximum 100 hosts allowed per import",
+2 -1
View File
@@ -548,10 +548,11 @@
"downloadSample": "下载示例", "downloadSample": "下载示例",
"formatGuide": "格式指南", "formatGuide": "格式指南",
"exportCredentialWarning": "警告:主机 \"{{name}}\" 使用凭据认证。导出的文件将不包含凭据数据,导入后需要手动重新配置。您确定要继续吗?", "exportCredentialWarning": "警告:主机 \"{{name}}\" 使用凭据认证。导出的文件将不包含凭据数据,导入后需要手动重新配置。您确定要继续吗?",
"exportSensitiveDataWarning": "警告:主机 \"{{name}}\" 包含敏感认证数据(密码/SSH密钥)。出于安全考虑,导出的文件将不包含此数据。导入后您需要重新配置认证。您确定要继续吗?", "exportSensitiveDataWarning": "警告:主机 \"{{name}}\" 包含敏感认证数据(密码/SSH密钥)。导出的文件将以明文形式包含这些数据。请妥善保管文件,使用后建议删除。您确定要继续吗?",
"uncategorized": "未分类", "uncategorized": "未分类",
"confirmDelete": "确定要删除 \"{{name}}\" 吗?", "confirmDelete": "确定要删除 \"{{name}}\" 吗?",
"failedToDeleteHost": "删除主机失败", "failedToDeleteHost": "删除主机失败",
"failedToExportHost": "导出主机失败。请确保您已登录并有权访问主机数据。",
"jsonMustContainHosts": "JSON 必须包含 \"hosts\" 数组或是一个主机数组", "jsonMustContainHosts": "JSON 必须包含 \"hosts\" 数组或是一个主机数组",
"noHostsInJson": "JSON 文件中未找到主机", "noHostsInJson": "JSON 文件中未找到主机",
"maxHostsAllowed": "每次导入最多允许 100 个主机", "maxHostsAllowed": "每次导入最多允许 100 个主机",
@@ -21,6 +21,7 @@ import {
bulkImportSSHHosts, bulkImportSSHHosts,
updateSSHHost, updateSSHHost,
renameFolder, renameFolder,
exportSSHHostWithCredentials,
} from "@/ui/main-axios.ts"; } from "@/ui/main-axios.ts";
import { toast } from "sonner"; import { toast } from "sonner";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -159,29 +160,16 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
performExport(host, actualAuthType); performExport(host, actualAuthType);
}; };
const performExport = (host: SSHHost, actualAuthType: string) => { const performExport = async (host: SSHHost, actualAuthType: string) => {
const exportData: any = { try {
name: host.name, // Fetch decrypted host data from backend
ip: host.ip, const decryptedHost = await exportSSHHostWithCredentials(host.id);
port: host.port,
username: host.username,
authType: actualAuthType,
folder: host.folder,
tags: host.tags,
pin: host.pin,
enableTerminal: host.enableTerminal,
enableTunnel: host.enableTunnel,
enableFileManager: host.enableFileManager,
defaultPath: host.defaultPath,
tunnelConnections: host.tunnelConnections,
};
if (actualAuthType === "credential") {
exportData.credentialId = null;
}
// Use decrypted data for export (includes password, key, etc.)
const cleanExportData = Object.fromEntries( const cleanExportData = Object.fromEntries(
Object.entries(exportData).filter(([_, value]) => value !== undefined), Object.entries(decryptedHost).filter(
([_, value]) => value !== undefined,
),
); );
const blob = new Blob([JSON.stringify(cleanExportData, null, 2)], { const blob = new Blob([JSON.stringify(cleanExportData, null, 2)], {
@@ -199,6 +187,9 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
toast.success( toast.success(
`Exported host configuration for ${host.name || host.username}@${host.ip}`, `Exported host configuration for ${host.name || host.username}@${host.ip}`,
); );
} catch (error) {
toast.error(t("hosts.failedToExportHost"));
}
}; };
const handleEdit = (host: SSHHost) => { const handleEdit = (host: SSHHost) => {
+11
View File
@@ -797,6 +797,17 @@ export async function getSSHHostById(hostId: number): Promise<SSHHost> {
} }
} }
export async function exportSSHHostWithCredentials(
hostId: number,
): Promise<SSHHost> {
try {
const response = await sshHostApi.get(`/db/host/${hostId}/export`);
return response.data;
} catch (error) {
handleApiError(error, "export SSH host with credentials");
}
}
// ============================================================================ // ============================================================================
// SSH AUTOSTART MANAGEMENT // SSH AUTOSTART MANAGEMENT
// ============================================================================ // ============================================================================