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) {
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,
);
+4 -4
View File
@@ -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",
]),
};
+88 -13
View File
@@ -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<string, string> = {
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<string, string[]> = {
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;
}
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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 隧道",
@@ -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",
@@ -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,
],
);
@@ -1257,17 +1257,6 @@ export function FileViewer({
</Button>
</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>
@@ -38,6 +38,8 @@ export function TerminalWindow({
const { t } = useTranslation();
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } =
useWindowManager();
const terminalRef = React.useRef<any>(null);
const resizeTimeoutRef = React.useRef<NodeJS.Timeout | null>(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}
>
<Terminal
ref={terminalRef}
hostConfig={hostConfig}
isVisible={!currentWindow.isMinimized}
initialPath={initialPath}