Fix login/migration errors from users. Improved file manager PDF and terminal resizing.
This commit is contained in:
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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",
|
||||
]),
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user