Fix login/migration errors from users. Improved file manager PDF and terminal resizing.

This commit is contained in:
LukeGus
2025-10-01 23:24:21 -05:00
parent 88974e2ad1
commit 1c8af91cc8
9 changed files with 162 additions and 35 deletions
+2 -1
View File
@@ -147,7 +147,7 @@ class DataCrypto {
if (needsUpdate) { if (needsUpdate) {
const updateQuery = ` const updateQuery = `
UPDATE ssh_credentials 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 = ? WHERE id = ?
`; `;
db.prepare(updateQuery).run( db.prepare(updateQuery).run(
@@ -155,6 +155,7 @@ class DataCrypto {
updatedRecord.key || null, updatedRecord.key || null,
updatedRecord.key_password || null, updatedRecord.key_password || null,
updatedRecord.private_key || null, updatedRecord.private_key || null,
updatedRecord.public_key || null,
record.id, record.id,
); );
+4 -4
View File
@@ -22,13 +22,13 @@ class FieldCrypto {
"totp_backup_codes", "totp_backup_codes",
"oidc_identifier", "oidc_identifier",
]), ]),
ssh_data: new Set(["password", "key", "keyPassword"]), ssh_data: new Set(["password", "key", "key_password"]),
ssh_credentials: new Set([ ssh_credentials: new Set([
"password", "password",
"privateKey", "private_key",
"keyPassword", "key_password",
"key", "key",
"publicKey", "public_key",
]), ]),
}; };
+88 -13
View File
@@ -2,6 +2,12 @@ import { FieldCrypto } from "./field-crypto.js";
import { databaseLogger } from "./logger.js"; import { databaseLogger } from "./logger.js";
export class LazyFieldEncryption { export class LazyFieldEncryption {
private static readonly LEGACY_FIELD_NAME_MAP: Record<string, string> = {
key_password: "keyPassword",
private_key: "privateKey",
public_key: "publicKey",
};
static isPlaintextField(value: string): boolean { static isPlaintextField(value: string): boolean {
if (!value) return false; if (!value) return false;
@@ -44,6 +50,20 @@ export class LazyFieldEncryption {
); );
return decrypted; return decrypted;
} catch (error) { } 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, { databaseLogger.error("Failed to decrypt field", error, {
operation: "lazy_encryption_decrypt_failed", operation: "lazy_encryption_decrypt_failed",
recordId, recordId,
@@ -60,9 +80,9 @@ export class LazyFieldEncryption {
userKEK: Buffer, userKEK: Buffer,
recordId: string, recordId: string,
fieldName: string, fieldName: string,
): { encrypted: string; wasPlaintext: boolean } { ): { encrypted: string; wasPlaintext: boolean; wasLegacyEncryption: boolean } {
if (!fieldValue) { if (!fieldValue) {
return { encrypted: "", wasPlaintext: false }; return { encrypted: "", wasPlaintext: false, wasLegacyEncryption: false };
} }
if (this.isPlaintextField(fieldValue)) { if (this.isPlaintextField(fieldValue)) {
@@ -74,7 +94,7 @@ export class LazyFieldEncryption {
fieldName, fieldName,
); );
return { encrypted, wasPlaintext: true }; return { encrypted, wasPlaintext: true, wasLegacyEncryption: false };
} catch (error) { } catch (error) {
databaseLogger.error("Failed to encrypt plaintext field", error, { databaseLogger.error("Failed to encrypt plaintext field", error, {
operation: "lazy_encryption_migrate_failed", operation: "lazy_encryption_migrate_failed",
@@ -85,7 +105,31 @@ export class LazyFieldEncryption {
throw error; throw error;
} }
} else { } 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) { for (const fieldName of sensitiveFields) {
const fieldValue = record[fieldName]; const fieldValue = record[fieldName];
if (fieldValue && this.isPlaintextField(fieldValue)) { if (fieldValue) {
try { try {
const { encrypted } = this.migrateFieldToEncrypted( const { encrypted, wasPlaintext, wasLegacyEncryption } = this.migrateFieldToEncrypted(
fieldValue, fieldValue,
userKEK, userKEK,
recordId, recordId,
fieldName, fieldName,
); );
updatedRecord[fieldName] = encrypted; if (wasPlaintext || wasLegacyEncryption) {
migratedFields.push(fieldName); updatedRecord[fieldName] = encrypted;
needsUpdate = true; migratedFields.push(fieldName);
needsUpdate = true;
}
} catch (error) { } catch (error) {
databaseLogger.error("Failed to migrate record field", error, { databaseLogger.error("Failed to migrate record field", error, {
operation: "lazy_encryption_record_field_failed", operation: "lazy_encryption_record_field_failed",
@@ -134,13 +180,42 @@ export class LazyFieldEncryption {
static getSensitiveFieldsForTable(tableName: string): string[] { static getSensitiveFieldsForTable(tableName: string): string[] {
const sensitiveFieldsMap: Record<string, string[]> = { const sensitiveFieldsMap: Record<string, string[]> = {
ssh_data: ["password", "key", "key_password"], 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"], users: ["totp_secret", "totp_backup_codes"],
}; };
return sensitiveFieldsMap[tableName] || []; 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( static async checkUserNeedsMigration(
userId: string, userId: string,
userKEK: Buffer, userKEK: Buffer,
@@ -169,7 +244,7 @@ export class LazyFieldEncryption {
const hostPlaintextFields: string[] = []; const hostPlaintextFields: string[] = [];
for (const field of sensitiveFields) { 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); hostPlaintextFields.push(field);
needsMigration = true; needsMigration = true;
} }
@@ -193,7 +268,7 @@ export class LazyFieldEncryption {
const credentialPlaintextFields: string[] = []; const credentialPlaintextFields: string[] = [];
for (const field of sensitiveFields) { 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); credentialPlaintextFields.push(field);
needsMigration = true; needsMigration = true;
} }
@@ -214,7 +289,7 @@ export class LazyFieldEncryption {
const userPlaintextFields: string[] = []; const userPlaintextFields: string[] = [];
for (const field of sensitiveFields) { 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); userPlaintextFields.push(field);
needsMigration = true; needsMigration = true;
} }
+10 -1
View File
@@ -978,7 +978,16 @@
"move": "Move", "move": "Move",
"searchInFile": "Search in file (Ctrl+F)", "searchInFile": "Search in file (Ctrl+F)",
"showKeyboardShortcuts": "Show keyboard shortcuts", "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": { "tunnels": {
"title": "SSH Tunnels", "title": "SSH Tunnels",
+10 -1
View File
@@ -969,7 +969,16 @@
"move": "移动", "move": "移动",
"searchInFile": "在文件中搜索 (Ctrl+F)", "searchInFile": "在文件中搜索 (Ctrl+F)",
"showKeyboardShortcuts": "显示键盘快捷键", "showKeyboardShortcuts": "显示键盘快捷键",
"startWritingMarkdown": "开始编写您的 markdown 内容..." "startWritingMarkdown": "开始编写您的 markdown 内容...",
"loadingFileComparison": "正在加载文件对比...",
"reload": "重新加载",
"compare": "对比",
"sideBySide": "并排显示",
"inline": "内嵌显示",
"fileComparison": "文件对比:{{file1}} 与 {{file2}}",
"fileTooLarge": "文件过大:{{error}}",
"sshConnectionFailed": "SSH 连接失败。请检查与 {{name}} ({{ip}}:{{port}}) 的连接",
"loadFileFailed": "加载文件失败:{{error}}"
}, },
"tunnels": { "tunnels": {
"title": "SSH 隧道", "title": "SSH 隧道",
@@ -17,7 +17,7 @@ import {
getSSHStatus, getSSHStatus,
connectSSH, connectSSH,
} from "@/ui/main-axios"; } from "@/ui/main-axios";
import type { FileItem, SSHHost } from "../../../../types/index.js"; import type { FileItem, SSHHost } from "@/types/index";
interface DiffViewerProps { interface DiffViewerProps {
file1: FileItem; file1: FileItem;
@@ -62,8 +62,22 @@ export function DiffViewer({
}); });
} }
} catch (error) { } catch (error) {
console.error("SSH connection check/reconnect failed:", error); try {
throw error; 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, automaticLayout: true,
readOnly: true, readOnly: true,
originalEditable: false, originalEditable: false,
modifiedEditable: false,
scrollbar: { scrollbar: {
vertical: "visible", vertical: "visible",
horizontal: "visible", horizontal: "visible",
@@ -15,6 +15,7 @@ interface DraggableWindowProps {
onClose: () => void; onClose: () => void;
onMinimize?: () => void; onMinimize?: () => void;
onMaximize?: () => void; onMaximize?: () => void;
onResize?: () => void;
isMaximized?: boolean; isMaximized?: boolean;
zIndex?: number; zIndex?: number;
onFocus?: () => void; onFocus?: () => void;
@@ -33,6 +34,7 @@ export function DraggableWindow({
onClose, onClose,
onMinimize, onMinimize,
onMaximize, onMaximize,
onResize,
isMaximized = false, isMaximized = false,
zIndex = 1000, zIndex = 1000,
onFocus, onFocus,
@@ -197,6 +199,10 @@ export function DraggableWindow({
setSize({ width: newWidth, height: newHeight }); setSize({ width: newWidth, height: newHeight });
setPosition({ x: newX, y: newY }); setPosition({ x: newX, y: newY });
if (onResize) {
onResize();
}
} }
}, },
[ [
@@ -211,6 +217,7 @@ export function DraggableWindow({
minWidth, minWidth,
minHeight, minHeight,
resizeDirection, resizeDirection,
onResize,
], ],
); );
@@ -1257,17 +1257,6 @@ export function FileViewer({
</Button> </Button>
</div> </div>
</div> </div>
{onDownload && (
<Button
variant="outline"
size="sm"
onClick={onDownload}
className="flex items-center gap-2"
>
<Download className="w-4 h-4" />
{t("fileManager.download")}
</Button>
)}
</div> </div>
</div> </div>
@@ -38,6 +38,8 @@ export function TerminalWindow({
const { t } = useTranslation(); const { t } = useTranslation();
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } = const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } =
useWindowManager(); useWindowManager();
const terminalRef = React.useRef<any>(null);
const resizeTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
const currentWindow = windows.find((w) => w.id === windowId); const currentWindow = windows.find((w) => w.id === windowId);
if (!currentWindow) { if (!currentWindow) {
@@ -60,6 +62,26 @@ export function TerminalWindow({
focusWindow(windowId); 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 const terminalTitle = executeCommand
? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand }) ? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand })
: initialPath : initialPath
@@ -81,10 +103,12 @@ export function TerminalWindow({
onClose={handleClose} onClose={handleClose}
onMaximize={handleMaximize} onMaximize={handleMaximize}
onFocus={handleFocus} onFocus={handleFocus}
onResize={handleResize}
isMaximized={currentWindow.isMaximized} isMaximized={currentWindow.isMaximized}
zIndex={currentWindow.zIndex} zIndex={currentWindow.zIndex}
> >
<Terminal <Terminal
ref={terminalRef}
hostConfig={hostConfig} hostConfig={hostConfig}
isVisible={!currentWindow.isMinimized} isVisible={!currentWindow.isMinimized}
initialPath={initialPath} initialPath={initialPath}