Fix critical UI and data export issues #327
@@ -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" });
|
||||
}
|
||||
|
The 
The `hostId` from `req.params.id` is used as a string and converted to a number in multiple places. This can lead to inconsistencies and potential issues if the parameter is not a valid number. It's more robust to parse and validate it once at the beginning of the handler. After this change, you can use the `hostId` variable directly as a number throughout the function, removing the need for `Number(hostId)` and `parseInt(hostId)`.
```suggestion
const hostId = parseInt(req.params.id, 10);
const userId = (req as any).userId;
if (!isNonEmptyString(userId) || isNaN(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)
|
||||
// DELETE /ssh/host/:id
|
||||
router.delete(
|
||||
|
||||
@@ -564,10 +564,11 @@
|
||||
"downloadSample": "Download Sample",
|
||||
"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?",
|
||||
"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",
|
||||
"confirmDelete": "Are you sure you want to delete \"{{name}}\" ?",
|
||||
"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",
|
||||
"noHostsInJson": "No hosts found in JSON file",
|
||||
"maxHostsAllowed": "Maximum 100 hosts allowed per import",
|
||||
|
||||
@@ -548,10 +548,11 @@
|
||||
"downloadSample": "下载示例",
|
||||
"formatGuide": "格式指南",
|
||||
"exportCredentialWarning": "警告:主机 \"{{name}}\" 使用凭据认证。导出的文件将不包含凭据数据,导入后需要手动重新配置。您确定要继续吗?",
|
||||
"exportSensitiveDataWarning": "警告:主机 \"{{name}}\" 包含敏感认证数据(密码/SSH密钥)。出于安全考虑,导出的文件将不包含此数据。导入后您需要重新配置认证。您确定要继续吗?",
|
||||
"exportSensitiveDataWarning": "警告:主机 \"{{name}}\" 包含敏感认证数据(密码/SSH密钥)。导出的文件将以明文形式包含这些数据。请妥善保管文件,使用后建议删除。您确定要继续吗?",
|
||||
"uncategorized": "未分类",
|
||||
"confirmDelete": "确定要删除 \"{{name}}\" 吗?",
|
||||
"failedToDeleteHost": "删除主机失败",
|
||||
"failedToExportHost": "导出主机失败。请确保您已登录并有权访问主机数据。",
|
||||
"jsonMustContainHosts": "JSON 必须包含 \"hosts\" 数组或是一个主机数组",
|
||||
"noHostsInJson": "JSON 文件中未找到主机",
|
||||
"maxHostsAllowed": "每次导入最多允许 100 个主机",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
bulkImportSSHHosts,
|
||||
updateSSHHost,
|
||||
renameFolder,
|
||||
exportSSHHostWithCredentials,
|
||||
} from "@/ui/main-axios.ts";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -159,29 +160,16 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
performExport(host, actualAuthType);
|
||||
};
|
||||
|
||||
const performExport = (host: SSHHost, actualAuthType: string) => {
|
||||
const exportData: any = {
|
||||
name: host.name,
|
||||
ip: host.ip,
|
||||
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;
|
||||
}
|
||||
const performExport = async (host: SSHHost, actualAuthType: string) => {
|
||||
try {
|
||||
// Fetch decrypted host data from backend
|
||||
const decryptedHost = await exportSSHHostWithCredentials(host.id);
|
||||
|
||||
// Use decrypted data for export (includes password, key, etc.)
|
||||
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)], {
|
||||
@@ -199,6 +187,9 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
toast.success(
|
||||
`Exported host configuration for ${host.name || host.username}@${host.ip}`,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(t("hosts.failedToExportHost"));
|
||||
|
The 
The `actualAuthType` parameter is passed to the `performExport` function but is no longer used within its body. Removing it will clean up the code. You will also need to update the call sites for this function in `handleExport`.
```suggestion
const performExport = async (host: SSHHost) => {
```
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (host: SSHHost) => {
|
||||
|
||||
@@ -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
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user
Using a
GETrequest to export sensitive data can be a security risk. GET requests, including their paths, are often logged by servers, proxies, and in browser history. It's a best practice to usePOSTfor actions that involve sensitive data to avoid leaking identifiers likehostIdin logs.You'll also need to update the corresponding frontend client in
src/ui/main-axios.tsto usesshHostApi.post.