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) {
|
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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user