diff --git a/src/backend/utils/data-crypto.ts b/src/backend/utils/data-crypto.ts index 37099a6a..bcad3f2e 100644 --- a/src/backend/utils/data-crypto.ts +++ b/src/backend/utils/data-crypto.ts @@ -147,7 +147,7 @@ class DataCrypto { if (needsUpdate) { const updateQuery = ` UPDATE ssh_credentials - SET password = ?, key = ?, key_password = ?, private_key = ?, updated_at = CURRENT_TIMESTAMP + SET password = ?, key = ?, key_password = ?, private_key = ?, public_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `; db.prepare(updateQuery).run( @@ -155,6 +155,7 @@ class DataCrypto { updatedRecord.key || null, updatedRecord.key_password || null, updatedRecord.private_key || null, + updatedRecord.public_key || null, record.id, ); diff --git a/src/backend/utils/field-crypto.ts b/src/backend/utils/field-crypto.ts index b225fa8b..098b5b8e 100644 --- a/src/backend/utils/field-crypto.ts +++ b/src/backend/utils/field-crypto.ts @@ -22,13 +22,13 @@ class FieldCrypto { "totp_backup_codes", "oidc_identifier", ]), - ssh_data: new Set(["password", "key", "keyPassword"]), + ssh_data: new Set(["password", "key", "key_password"]), ssh_credentials: new Set([ "password", - "privateKey", - "keyPassword", + "private_key", + "key_password", "key", - "publicKey", + "public_key", ]), }; diff --git a/src/backend/utils/lazy-field-encryption.ts b/src/backend/utils/lazy-field-encryption.ts index 685fa25a..c40cd09d 100644 --- a/src/backend/utils/lazy-field-encryption.ts +++ b/src/backend/utils/lazy-field-encryption.ts @@ -2,6 +2,12 @@ import { FieldCrypto } from "./field-crypto.js"; import { databaseLogger } from "./logger.js"; export class LazyFieldEncryption { + private static readonly LEGACY_FIELD_NAME_MAP: Record = { + key_password: "keyPassword", + private_key: "privateKey", + public_key: "publicKey", + }; + static isPlaintextField(value: string): boolean { if (!value) return false; @@ -44,6 +50,20 @@ export class LazyFieldEncryption { ); return decrypted; } catch (error) { + const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName]; + if (legacyFieldName) { + try { + const decrypted = FieldCrypto.decryptField( + fieldValue, + userKEK, + recordId, + legacyFieldName, + ); + return decrypted; + } catch (legacyError) { + } + } + databaseLogger.error("Failed to decrypt field", error, { operation: "lazy_encryption_decrypt_failed", recordId, @@ -60,9 +80,9 @@ export class LazyFieldEncryption { userKEK: Buffer, recordId: string, fieldName: string, - ): { encrypted: string; wasPlaintext: boolean } { + ): { encrypted: string; wasPlaintext: boolean; wasLegacyEncryption: boolean } { if (!fieldValue) { - return { encrypted: "", wasPlaintext: false }; + return { encrypted: "", wasPlaintext: false, wasLegacyEncryption: false }; } if (this.isPlaintextField(fieldValue)) { @@ -74,7 +94,7 @@ export class LazyFieldEncryption { fieldName, ); - return { encrypted, wasPlaintext: true }; + return { encrypted, wasPlaintext: true, wasLegacyEncryption: false }; } catch (error) { databaseLogger.error("Failed to encrypt plaintext field", error, { operation: "lazy_encryption_migrate_failed", @@ -85,7 +105,31 @@ export class LazyFieldEncryption { throw error; } } else { - return { encrypted: fieldValue, wasPlaintext: false }; + try { + FieldCrypto.decryptField(fieldValue, userKEK, recordId, fieldName); + return { encrypted: fieldValue, wasPlaintext: false, wasLegacyEncryption: false }; + } catch (error) { + const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName]; + if (legacyFieldName) { + try { + const decrypted = FieldCrypto.decryptField( + fieldValue, + userKEK, + recordId, + legacyFieldName, + ); + const reencrypted = FieldCrypto.encryptField( + decrypted, + userKEK, + recordId, + fieldName, + ); + return { encrypted: reencrypted, wasPlaintext: false, wasLegacyEncryption: true }; + } catch (legacyError) { + } + } + return { encrypted: fieldValue, wasPlaintext: false, wasLegacyEncryption: false }; + } } } @@ -106,18 +150,20 @@ export class LazyFieldEncryption { for (const fieldName of sensitiveFields) { const fieldValue = record[fieldName]; - if (fieldValue && this.isPlaintextField(fieldValue)) { + if (fieldValue) { try { - const { encrypted } = this.migrateFieldToEncrypted( + const { encrypted, wasPlaintext, wasLegacyEncryption } = this.migrateFieldToEncrypted( fieldValue, userKEK, recordId, fieldName, ); - updatedRecord[fieldName] = encrypted; - migratedFields.push(fieldName); - needsUpdate = true; + if (wasPlaintext || wasLegacyEncryption) { + updatedRecord[fieldName] = encrypted; + migratedFields.push(fieldName); + needsUpdate = true; + } } catch (error) { databaseLogger.error("Failed to migrate record field", error, { operation: "lazy_encryption_record_field_failed", @@ -134,13 +180,42 @@ export class LazyFieldEncryption { static getSensitiveFieldsForTable(tableName: string): string[] { const sensitiveFieldsMap: Record = { ssh_data: ["password", "key", "key_password"], - ssh_credentials: ["password", "key", "key_password", "private_key"], + ssh_credentials: ["password", "key", "key_password", "private_key", "public_key"], users: ["totp_secret", "totp_backup_codes"], }; return sensitiveFieldsMap[tableName] || []; } + static fieldNeedsMigration( + fieldValue: string, + userKEK: Buffer, + recordId: string, + fieldName: string, + ): boolean { + if (!fieldValue) return false; + + if (this.isPlaintextField(fieldValue)) { + return true; + } + + try { + FieldCrypto.decryptField(fieldValue, userKEK, recordId, fieldName); + return false; + } catch (error) { + const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName]; + if (legacyFieldName) { + try { + FieldCrypto.decryptField(fieldValue, userKEK, recordId, legacyFieldName); + return true; + } catch (legacyError) { + return false; + } + } + return false; + } + } + static async checkUserNeedsMigration( userId: string, userKEK: Buffer, @@ -169,7 +244,7 @@ export class LazyFieldEncryption { const hostPlaintextFields: string[] = []; for (const field of sensitiveFields) { - if (host[field] && this.isPlaintextField(host[field])) { + if (host[field] && this.fieldNeedsMigration(host[field], userKEK, host.id.toString(), field)) { hostPlaintextFields.push(field); needsMigration = true; } @@ -193,7 +268,7 @@ export class LazyFieldEncryption { const credentialPlaintextFields: string[] = []; for (const field of sensitiveFields) { - if (credential[field] && this.isPlaintextField(credential[field])) { + if (credential[field] && this.fieldNeedsMigration(credential[field], userKEK, credential.id.toString(), field)) { credentialPlaintextFields.push(field); needsMigration = true; } @@ -214,7 +289,7 @@ export class LazyFieldEncryption { const userPlaintextFields: string[] = []; for (const field of sensitiveFields) { - if (user[field] && this.isPlaintextField(user[field])) { + if (user[field] && this.fieldNeedsMigration(user[field], userKEK, userId, field)) { userPlaintextFields.push(field); needsMigration = true; } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 262cec0a..b062e4c5 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -978,7 +978,16 @@ "move": "Move", "searchInFile": "Search in file (Ctrl+F)", "showKeyboardShortcuts": "Show keyboard shortcuts", - "startWritingMarkdown": "Start writing your markdown content..." + "startWritingMarkdown": "Start writing your markdown content...", + "loadingFileComparison": "Loading file comparison...", + "reload": "Reload", + "compare": "Compare", + "sideBySide": "Side by Side", + "inline": "Inline", + "fileComparison": "File Comparison: {{file1}} vs {{file2}}", + "fileTooLarge": "File too large: {{error}}", + "sshConnectionFailed": "SSH connection failed. Please check your connection to {{name}} ({{ip}}:{{port}})", + "loadFileFailed": "Failed to load file: {{error}}" }, "tunnels": { "title": "SSH Tunnels", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 5b7ba6b1..e3a8cfaf 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -969,7 +969,16 @@ "move": "移动", "searchInFile": "在文件中搜索 (Ctrl+F)", "showKeyboardShortcuts": "显示键盘快捷键", - "startWritingMarkdown": "开始编写您的 markdown 内容..." + "startWritingMarkdown": "开始编写您的 markdown 内容...", + "loadingFileComparison": "正在加载文件对比...", + "reload": "重新加载", + "compare": "对比", + "sideBySide": "并排显示", + "inline": "内嵌显示", + "fileComparison": "文件对比:{{file1}} 与 {{file2}}", + "fileTooLarge": "文件过大:{{error}}", + "sshConnectionFailed": "SSH 连接失败。请检查与 {{name}} ({{ip}}:{{port}}) 的连接", + "loadFileFailed": "加载文件失败:{{error}}" }, "tunnels": { "title": "SSH 隧道", diff --git a/src/ui/Desktop/Apps/File Manager/components/DiffViewer.tsx b/src/ui/Desktop/Apps/File Manager/components/DiffViewer.tsx index 90c4b609..e9312924 100644 --- a/src/ui/Desktop/Apps/File Manager/components/DiffViewer.tsx +++ b/src/ui/Desktop/Apps/File Manager/components/DiffViewer.tsx @@ -17,7 +17,7 @@ import { getSSHStatus, connectSSH, } from "@/ui/main-axios"; -import type { FileItem, SSHHost } from "../../../../types/index.js"; +import type { FileItem, SSHHost } from "@/types/index"; interface DiffViewerProps { file1: FileItem; @@ -62,8 +62,22 @@ export function DiffViewer({ }); } } catch (error) { - console.error("SSH connection check/reconnect failed:", error); - throw error; + try { + await connectSSH(sshSessionId, { + hostId: sshHost.id, + ip: sshHost.ip, + port: sshHost.port, + username: sshHost.username, + password: sshHost.password, + sshKey: sshHost.key, + keyPassword: sshHost.keyPassword, + authType: sshHost.authType, + credentialId: sshHost.credentialId, + userId: sshHost.userId, + }); + } catch (reconnectError) { + throw reconnectError; + } } }; @@ -310,7 +324,6 @@ export function DiffViewer({ automaticLayout: true, readOnly: true, originalEditable: false, - modifiedEditable: false, scrollbar: { vertical: "visible", horizontal: "visible", diff --git a/src/ui/Desktop/Apps/File Manager/components/DraggableWindow.tsx b/src/ui/Desktop/Apps/File Manager/components/DraggableWindow.tsx index d214ab93..76082fd5 100644 --- a/src/ui/Desktop/Apps/File Manager/components/DraggableWindow.tsx +++ b/src/ui/Desktop/Apps/File Manager/components/DraggableWindow.tsx @@ -15,6 +15,7 @@ interface DraggableWindowProps { onClose: () => void; onMinimize?: () => void; onMaximize?: () => void; + onResize?: () => void; isMaximized?: boolean; zIndex?: number; onFocus?: () => void; @@ -33,6 +34,7 @@ export function DraggableWindow({ onClose, onMinimize, onMaximize, + onResize, isMaximized = false, zIndex = 1000, onFocus, @@ -197,6 +199,10 @@ export function DraggableWindow({ setSize({ width: newWidth, height: newHeight }); setPosition({ x: newX, y: newY }); + + if (onResize) { + onResize(); + } } }, [ @@ -211,6 +217,7 @@ export function DraggableWindow({ minWidth, minHeight, resizeDirection, + onResize, ], ); diff --git a/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx b/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx index 651e8ca1..1a3cb109 100644 --- a/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx +++ b/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx @@ -1257,17 +1257,6 @@ export function FileViewer({ - {onDownload && ( - - )} diff --git a/src/ui/Desktop/Apps/File Manager/components/TerminalWindow.tsx b/src/ui/Desktop/Apps/File Manager/components/TerminalWindow.tsx index 6069e8a7..2f56a32d 100644 --- a/src/ui/Desktop/Apps/File Manager/components/TerminalWindow.tsx +++ b/src/ui/Desktop/Apps/File Manager/components/TerminalWindow.tsx @@ -38,6 +38,8 @@ export function TerminalWindow({ const { t } = useTranslation(); const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } = useWindowManager(); + const terminalRef = React.useRef(null); + const resizeTimeoutRef = React.useRef(null); const currentWindow = windows.find((w) => w.id === windowId); if (!currentWindow) { @@ -60,6 +62,26 @@ export function TerminalWindow({ focusWindow(windowId); }; + const handleResize = () => { + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + } + + resizeTimeoutRef.current = setTimeout(() => { + if (terminalRef.current?.fit) { + terminalRef.current.fit(); + } + }, 100); + }; + + React.useEffect(() => { + return () => { + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + } + }; + }, []); + const terminalTitle = executeCommand ? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand }) : initialPath @@ -81,10 +103,12 @@ export function TerminalWindow({ onClose={handleClose} onMaximize={handleMaximize} onFocus={handleFocus} + onResize={handleResize} isMaximized={currentWindow.isMaximized} zIndex={currentWindow.zIndex} >