v1.7.2 #364
@@ -10,6 +10,9 @@ http {
|
||||
keepalive_timeout 65;
|
||||
client_header_timeout 300s;
|
||||
|
||||
set_real_ip_from 127.0.0.1;
|
||||
real_ip_header X-Forwarded-For;
|
||||
|
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
@@ -23,7 +26,6 @@ http {
|
||||
return 301 https://$host:${SSL_PORT}$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS Server
|
||||
server {
|
||||
listen ${SSL_PORT} ssl;
|
||||
server_name _;
|
||||
@@ -41,7 +43,6 @@ http {
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
# Handle missing source map files gracefully
|
||||
location ~* \.map$ {
|
||||
return 404;
|
||||
access_log off;
|
||||
|
||||
@@ -10,6 +10,9 @@ http {
|
||||
keepalive_timeout 65;
|
||||
client_header_timeout 300s;
|
||||
|
||||
set_real_ip_from 127.0.0.1;
|
||||
real_ip_header X-Forwarded-For;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
@@ -29,7 +32,6 @@ http {
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
# Handle missing source map files gracefully
|
||||
location ~* \.map$ {
|
||||
return 404;
|
||||
access_log off;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"private": true,
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.2",
|
||||
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
||||
"author": "Karmaa",
|
||||
"main": "electron/main.cjs",
|
||||
|
||||
@@ -46,7 +46,7 @@ export const sshData = sqliteTable("ssh_data", {
|
||||
|
||||
password: text("password"),
|
||||
key: text("key", { length: 8192 }),
|
||||
keyPassword: text("key_password"),
|
||||
key_password: text("key_password"),
|
||||
keyType: text("key_type"),
|
||||
|
||||
|
I see you're changing some field names from camelCase to snake_case (e.g., This mix of naming conventions can be confusing and lead to bugs. It would be best to choose one convention (preferably snake_case for database schemas) and apply it to all fields for better maintainability. 
I see you're changing some field names from camelCase to snake_case (e.g., `keyPassword` to `key_password`), which is great for consistency with typical SQL naming conventions. However, this change isn't applied consistently across the schema. For example, in the `sshData` table, fields like `userId`, `authType`, `credentialId`, `autostartPassword`, and `keyType` remain in camelCase. The `sshCredentials` table also has a mix of conventions (`keyType`, `detectedKeyType`, `usageCount`).
This mix of naming conventions can be confusing and lead to bugs. It would be best to choose one convention (preferably snake_case for database schemas) and apply it to all fields for better maintainability.
|
||||
autostartPassword: text("autostart_password"),
|
||||
@@ -142,9 +142,9 @@ export const sshCredentials = sqliteTable("ssh_credentials", {
|
||||
username: text("username").notNull(),
|
||||
password: text("password"),
|
||||
key: text("key", { length: 16384 }),
|
||||
privateKey: text("private_key", { length: 16384 }),
|
||||
publicKey: text("public_key", { length: 4096 }),
|
||||
keyPassword: text("key_password"),
|
||||
private_key: text("private_key", { length: 16384 }),
|
||||
public_key: text("public_key", { length: 4096 }),
|
||||
key_password: text("key_password"),
|
||||
keyType: text("key_type"),
|
||||
detectedKeyType: text("detected_key_type"),
|
||||
usageCount: integer("usage_count").notNull().default(0),
|
||||
|
||||
@@ -174,9 +174,9 @@ router.post(
|
||||
username: username.trim(),
|
||||
password: plainPassword,
|
||||
key: plainKey,
|
||||
privateKey: keyInfo?.privateKey || plainKey,
|
||||
publicKey: keyInfo?.publicKey || null,
|
||||
keyPassword: plainKeyPassword,
|
||||
private_key: keyInfo?.privateKey || plainKey,
|
||||
public_key: keyInfo?.publicKey || null,
|
||||
key_password: plainKeyPassword,
|
||||
keyType: keyType || null,
|
||||
detectedKeyType: keyInfo?.keyType || null,
|
||||
usageCount: 0,
|
||||
@@ -424,13 +424,13 @@ router.put(
|
||||
error: `Invalid SSH key: ${keyInfo.error}`,
|
||||
});
|
||||
}
|
||||
updateFields.privateKey = keyInfo.privateKey;
|
||||
updateFields.publicKey = keyInfo.publicKey;
|
||||
updateFields.private_key = keyInfo.privateKey;
|
||||
updateFields.public_key = keyInfo.publicKey;
|
||||
updateFields.detectedKeyType = keyInfo.keyType;
|
||||
}
|
||||
}
|
||||
if (updateData.keyPassword !== undefined) {
|
||||
updateFields.keyPassword = updateData.keyPassword || null;
|
||||
updateFields.key_password = updateData.keyPassword || null;
|
||||
}
|
||||
|
||||
if (Object.keys(updateFields).length === 0) {
|
||||
@@ -537,7 +537,7 @@ router.delete(
|
||||
credentialId: null,
|
||||
password: null,
|
||||
key: null,
|
||||
keyPassword: null,
|
||||
key_password: null,
|
||||
authType: "password",
|
||||
})
|
||||
.where(
|
||||
@@ -633,7 +633,7 @@ router.post(
|
||||
authType: credential.auth_type || credential.authType,
|
||||
password: null,
|
||||
key: null,
|
||||
keyPassword: null,
|
||||
key_password: null,
|
||||
keyType: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
@@ -91,7 +91,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
|
||||
username: host.username,
|
||||
password: host.autostartPassword,
|
||||
key: host.autostartKey,
|
||||
keyPassword: host.autostartKeyPassword,
|
||||
key_password: host.autostartKeyPassword,
|
||||
autostartPassword: host.autostartPassword,
|
||||
autostartKey: host.autostartKey,
|
||||
autostartKeyPassword: host.autostartKeyPassword,
|
||||
@@ -151,7 +151,7 @@ router.get("/db/host/internal/all", async (req: Request, res: Response) => {
|
||||
username: host.username,
|
||||
password: host.autostartPassword || host.password,
|
||||
key: host.autostartKey || host.key,
|
||||
keyPassword: host.autostartKeyPassword || host.keyPassword,
|
||||
key_password: host.autostartKeyPassword || host.key_password,
|
||||
autostartPassword: host.autostartPassword,
|
||||
autostartKey: host.autostartKey,
|
||||
autostartKeyPassword: host.autostartKeyPassword,
|
||||
@@ -226,7 +226,7 @@ router.post(
|
||||
authType,
|
||||
credentialId,
|
||||
key,
|
||||
keyPassword,
|
||||
key_password,
|
||||
keyType,
|
||||
pin,
|
||||
enableTerminal,
|
||||
@@ -274,17 +274,17 @@ router.post(
|
||||
if (effectiveAuthType === "password") {
|
||||
sshDataObj.password = password || null;
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
} else if (effectiveAuthType === "key") {
|
||||
sshDataObj.key = key || null;
|
||||
sshDataObj.keyPassword = keyPassword || null;
|
||||
sshDataObj.key_password = key_password || null;
|
||||
sshDataObj.keyType = keyType;
|
||||
sshDataObj.password = null;
|
||||
} else {
|
||||
sshDataObj.password = null;
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
@@ -407,7 +407,7 @@ router.put(
|
||||
authType,
|
||||
credentialId,
|
||||
key,
|
||||
keyPassword,
|
||||
key_password,
|
||||
keyType,
|
||||
pin,
|
||||
enableTerminal,
|
||||
@@ -458,14 +458,14 @@ router.put(
|
||||
sshDataObj.password = password;
|
||||
}
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
} else if (effectiveAuthType === "key") {
|
||||
if (key) {
|
||||
sshDataObj.key = key;
|
||||
}
|
||||
if (keyPassword !== undefined) {
|
||||
sshDataObj.keyPassword = keyPassword || null;
|
||||
if (key_password !== undefined) {
|
||||
sshDataObj.key_password = key_password || null;
|
||||
}
|
||||
if (keyType) {
|
||||
sshDataObj.keyType = keyType;
|
||||
@@ -474,7 +474,7 @@ router.put(
|
||||
} else {
|
||||
sshDataObj.password = null;
|
||||
sshDataObj.key = null;
|
||||
sshDataObj.keyPassword = null;
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
@@ -711,7 +711,7 @@ router.get(
|
||||
authType: resolvedHost.authType,
|
||||
password: resolvedHost.password || null,
|
||||
key: resolvedHost.key || null,
|
||||
keyPassword: resolvedHost.keyPassword || null,
|
||||
key_password: resolvedHost.key_password || null,
|
||||
keyType: resolvedHost.keyType || null,
|
||||
folder: resolvedHost.folder,
|
||||
tags:
|
||||
@@ -1234,7 +1234,7 @@ async function resolveHostCredentials(host: any): Promise<any> {
|
||||
authType: credential.auth_type || credential.authType,
|
||||
password: credential.password,
|
||||
key: credential.key,
|
||||
keyPassword: credential.key_password || credential.keyPassword,
|
||||
key_password: credential.key_password || credential.key_password,
|
||||
keyType: credential.key_type || credential.keyType,
|
||||
};
|
||||
}
|
||||
@@ -1404,8 +1404,8 @@ router.post(
|
||||
credentialId:
|
||||
hostData.authType === "credential" ? hostData.credentialId : null,
|
||||
key: hostData.authType === "key" ? hostData.key : null,
|
||||
keyPassword:
|
||||
hostData.authType === "key" ? hostData.keyPassword : null,
|
||||
key_password:
|
||||
hostData.authType === "key" ? hostData.key_password : null,
|
||||
keyType:
|
||||
hostData.authType === "key" ? hostData.keyType || "auto" : null,
|
||||
pin: hostData.pin || false,
|
||||
@@ -1540,7 +1540,7 @@ router.post(
|
||||
...tunnel,
|
||||
endpointPassword: decryptedEndpoint.password || null,
|
||||
endpointKey: decryptedEndpoint.key || null,
|
||||
endpointKeyPassword: decryptedEndpoint.keyPassword || null,
|
||||
endpointKeyPassword: decryptedEndpoint.key_password || null,
|
||||
endpointAuthType: endpointHost.authType,
|
||||
};
|
||||
}
|
||||
@@ -1563,7 +1563,7 @@ router.post(
|
||||
.set({
|
||||
autostartPassword: decryptedConfig.password || null,
|
||||
autostartKey: decryptedConfig.key || null,
|
||||
autostartKeyPassword: decryptedConfig.keyPassword || null,
|
||||
autostartKeyPassword: decryptedConfig.key_password || null,
|
||||
tunnelConnections: updatedTunnelConnections,
|
||||
})
|
||||
.where(eq(sshData.id, sshConfigId));
|
||||
|
||||
@@ -6,6 +6,20 @@ export class LazyFieldEncryption {
|
||||
key_password: "keyPassword",
|
||||
private_key: "privateKey",
|
||||
public_key: "publicKey",
|
||||
password_hash: "passwordHash",
|
||||
client_secret: "clientSecret",
|
||||
totp_secret: "totpSecret",
|
||||
totp_backup_codes: "totpBackupCodes",
|
||||
oidc_identifier: "oidcIdentifier",
|
||||
|
||||
keyPassword: "key_password",
|
||||
privateKey: "private_key",
|
||||
publicKey: "public_key",
|
||||
passwordHash: "password_hash",
|
||||
clientSecret: "client_secret",
|
||||
totpSecret: "totp_secret",
|
||||
totpBackupCodes: "totp_backup_codes",
|
||||
oidcIdentifier: "oidc_identifier",
|
||||
};
|
||||
|
||||
static isPlaintextField(value: string): boolean {
|
||||
|
||||
@@ -70,7 +70,36 @@ class UserCrypto {
|
||||
}
|
||||
|
||||
async setupOIDCUserEncryption(userId: string): Promise<void> {
|
||||
const DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH);
|
||||
const existingEncryptedDEK = await this.getEncryptedDEK(userId);
|
||||
|
||||
let DEK: Buffer;
|
||||
|
||||
if (existingEncryptedDEK) {
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
DEK = this.decryptDEK(existingEncryptedDEK, systemKey);
|
||||
systemKey.fill(0);
|
||||
} else {
|
||||
DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH);
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
|
||||
try {
|
||||
const encryptedDEK = this.encryptDEK(DEK, systemKey);
|
||||
await this.storeEncryptedDEK(userId, encryptedDEK);
|
||||
|
||||
const storedEncryptedDEK = await this.getEncryptedDEK(userId);
|
||||
if (
|
||||
storedEncryptedDEK &&
|
||||
storedEncryptedDEK.data !== encryptedDEK.data
|
||||
) {
|
||||
DEK.fill(0);
|
||||
DEK = this.decryptDEK(storedEncryptedDEK, systemKey);
|
||||
} else if (!storedEncryptedDEK) {
|
||||
throw new Error("Failed to store and retrieve user encryption key.");
|
||||
}
|
||||
} finally {
|
||||
systemKey.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
this.userSessions.set(userId, {
|
||||
@@ -134,20 +163,14 @@ class UserCrypto {
|
||||
|
||||
async authenticateOIDCUser(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const kekSalt = await this.getKEKSalt(userId);
|
||||
if (!kekSalt) {
|
||||
const encryptedDEK = await this.getEncryptedDEK(userId);
|
||||
|
||||
if (!encryptedDEK) {
|
||||
await this.setupOIDCUserEncryption(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
const systemKey = this.deriveOIDCSystemKey(userId);
|
||||
const encryptedDEK = await this.getEncryptedDEK(userId);
|
||||
if (!encryptedDEK) {
|
||||
systemKey.fill(0);
|
||||
await this.setupOIDCUserEncryption(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
const DEK = this.decryptDEK(encryptedDEK, systemKey);
|
||||
systemKey.fill(0);
|
||||
|
||||
|
||||
@@ -844,8 +844,13 @@
|
||||
"selectServerToEdit": "Select a server from the sidebar to start editing files",
|
||||
"fileOperations": "File Operations",
|
||||
"confirmDeleteMessage": "Are you sure you want to delete <strong>{{name}}</strong>?",
|
||||
"confirmDeleteSingleItem": "Are you sure you want to permanently delete \"{{name}}\"?",
|
||||
"confirmDeleteMultipleItems": "Are you sure you want to permanently delete {{count}} items?",
|
||||
"confirmDeleteMultipleItemsWithFolders": "Are you sure you want to permanently delete {{count}} items? This includes folders and their contents.",
|
||||
"confirmDeleteFolder": "Are you sure you want to permanently delete the folder \"{{name}}\" and all its contents?",
|
||||
"deleteDirectoryWarning": "This will delete the folder and all its contents.",
|
||||
"actionCannotBeUndone": "This action cannot be undone.",
|
||||
"permanentDeleteWarning": "This action cannot be undone. The item(s) will be permanently deleted from the server.",
|
||||
"recent": "Recent",
|
||||
"pinned": "Pinned",
|
||||
"folderShortcuts": "Folder Shortcuts",
|
||||
|
||||
@@ -852,8 +852,13 @@
|
||||
"selectServerToEdit": "从侧边栏选择服务器以开始编辑文件",
|
||||
"fileOperations": "文件操作",
|
||||
"confirmDeleteMessage": "确定要删除 <strong>{{name}}</strong> 吗?",
|
||||
"confirmDeleteSingleItem": "确定要永久删除 \"{{name}}\" 吗?",
|
||||
"confirmDeleteMultipleItems": "确定要永久删除 {{count}} 个项目吗?",
|
||||
"confirmDeleteMultipleItemsWithFolders": "确定要永久删除 {{count}} 个项目吗?这包括文件夹及其内容。",
|
||||
"confirmDeleteFolder": "确定要永久删除文件夹 \"{{name}}\" 及其所有内容吗?",
|
||||
"deleteDirectoryWarning": "这将删除文件夹及其所有内容。",
|
||||
"actionCannotBeUndone": "此操作无法撤销。",
|
||||
"permanentDeleteWarning": "此操作无法撤销。项目将从服务器永久删除。",
|
||||
"dragSystemFilesToUpload": "拖拽系统文件到此处上传",
|
||||
"dragFilesToWindowToDownload": "拖拽文件到窗口外下载",
|
||||
"openTerminalHere": "在此处打开终端",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { FileWindow } from "./components/FileWindow";
|
||||
import { DiffWindow } from "./components/DiffWindow";
|
||||
import { useDragToDesktop } from "../../../hooks/useDragToDesktop";
|
||||
import { useDragToSystemDesktop } from "../../../hooks/useDragToSystemDesktop";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from "sonner";
|
||||
@@ -82,6 +83,7 @@ function formatFileSize(bytes?: number): string {
|
||||
function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const { openWindow } = useWindowManager();
|
||||
const { t } = useTranslation();
|
||||
const { confirmWithToast } = useConfirmation();
|
||||
|
||||
const [currentHost, setCurrentHost] = useState<SSHHost | null>(
|
||||
initialHost || null,
|
||||
@@ -587,54 +589,85 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
async function handleDeleteFiles(files: FileItem[]) {
|
||||
if (!sshSessionId || files.length === 0) return;
|
||||
|
||||
try {
|
||||
await ensureSSHConnection();
|
||||
|
||||
for (const file of files) {
|
||||
await deleteSSHItem(
|
||||
sshSessionId,
|
||||
file.path,
|
||||
file.type === "directory",
|
||||
currentHost?.id,
|
||||
currentHost?.userId?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
const deletedFiles = files.map((file) => ({
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
}));
|
||||
|
||||
const undoAction: UndoAction = {
|
||||
type: "delete",
|
||||
description: t("fileManager.deletedItems", { count: files.length }),
|
||||
data: {
|
||||
operation: "cut",
|
||||
deletedFiles,
|
||||
targetDirectory: currentPath,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setUndoHistory((prev) => [...prev.slice(-9), undoAction]);
|
||||
|
||||
toast.success(
|
||||
t("fileManager.itemsDeletedSuccessfully", { count: files.length }),
|
||||
);
|
||||
handleRefreshDirectory();
|
||||
clearSelection();
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
|
||||
);
|
||||
let confirmMessage: string;
|
||||
if (files.length === 1) {
|
||||
const file = files[0];
|
||||
if (file.type === "directory") {
|
||||
confirmMessage = t("fileManager.confirmDeleteFolder", {
|
||||
name: file.name,
|
||||
});
|
||||
} else {
|
||||
toast.error(t("fileManager.failedToDeleteItems"));
|
||||
confirmMessage = t("fileManager.confirmDeleteSingleItem", {
|
||||
name: file.name,
|
||||
});
|
||||
}
|
||||
console.error("Delete failed:", error);
|
||||
} else {
|
||||
const hasDirectory = files.some((file) => file.type === "directory");
|
||||
const translationKey = hasDirectory
|
||||
? "fileManager.confirmDeleteMultipleItemsWithFolders"
|
||||
: "fileManager.confirmDeleteMultipleItems";
|
||||
|
||||
confirmMessage = t(translationKey, {
|
||||
count: files.length,
|
||||
});
|
||||
}
|
||||
|
||||
const fullMessage = `${confirmMessage}\n\n${t("fileManager.permanentDeleteWarning")}`;
|
||||
|
||||
confirmWithToast(
|
||||
fullMessage,
|
||||
async () => {
|
||||
try {
|
||||
await ensureSSHConnection();
|
||||
|
||||
for (const file of files) {
|
||||
await deleteSSHItem(
|
||||
sshSessionId,
|
||||
file.path,
|
||||
file.type === "directory",
|
||||
currentHost?.id,
|
||||
currentHost?.userId?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
const deletedFiles = files.map((file) => ({
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
}));
|
||||
|
||||
const undoAction: UndoAction = {
|
||||
type: "delete",
|
||||
description: t("fileManager.deletedItems", { count: files.length }),
|
||||
data: {
|
||||
operation: "cut",
|
||||
deletedFiles,
|
||||
targetDirectory: currentPath,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setUndoHistory((prev) => [...prev.slice(-9), undoAction]);
|
||||
|
||||
toast.success(
|
||||
t("fileManager.itemsDeletedSuccessfully", { count: files.length }),
|
||||
);
|
||||
handleRefreshDirectory();
|
||||
clearSelection();
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
|
||||
);
|
||||
} else {
|
||||
toast.error(t("fileManager.failedToDeleteItems"));
|
||||
}
|
||||
console.error("Delete failed:", error);
|
||||
}
|
||||
},
|
||||
"destructive",
|
||||
);
|
||||
}
|
||||
|
||||
function handleCreateNewFolder() {
|
||||
|
||||
@@ -210,7 +210,18 @@ export function HostManagerEditor({
|
||||
defaultPath: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.authType === "key") {
|
||||
if (data.authType === "password") {
|
||||
if (
|
||||
!data.password ||
|
||||
(typeof data.password === "string" && data.password.trim() === "")
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t("hosts.passwordRequired"),
|
||||
path: ["password"],
|
||||
});
|
||||
}
|
||||
} else if (data.authType === "key") {
|
||||
if (
|
||||
!data.key ||
|
||||
(typeof data.key === "string" && data.key.trim() === "")
|
||||
|
||||
@@ -24,7 +24,10 @@ function AppContent() {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [authLoading, setAuthLoading] = useState(true);
|
||||
const [showVersionCheck, setShowVersionCheck] = useState(true);
|
||||
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(true);
|
||||
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(() => {
|
||||
const saved = localStorage.getItem("topNavbarOpen");
|
||||
return saved !== null ? JSON.parse(saved) : true;
|
||||
});
|
||||
const { currentTab, tabs } = useTabs();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -64,6 +67,10 @@ function AppContent() {
|
||||
return () => window.removeEventListener("storage", handleStorageChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("topNavbarOpen", JSON.stringify(isTopbarOpen));
|
||||
}, [isTopbarOpen]);
|
||||
|
||||
const handleSelectView = (nextView: string) => {
|
||||
setMountedViews((prev) => {
|
||||
if (prev.has(nextView)) return prev;
|
||||
|
||||
@@ -101,7 +101,10 @@ export function LeftSidebar({
|
||||
const [deleteLoading, setDeleteLoading] = React.useState(false);
|
||||
const [deleteError, setDeleteError] = React.useState<string | null>(null);
|
||||
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(() => {
|
||||
const saved = localStorage.getItem("leftSidebarOpen");
|
||||
return saved !== null ? JSON.parse(saved) : true;
|
||||
});
|
||||
|
||||
const {
|
||||
tabs: tabList,
|
||||
@@ -181,7 +184,6 @@ export function LeftSidebar({
|
||||
newHost.key !== existingHost.key ||
|
||||
newHost.keyPassword !== existingHost.keyPassword ||
|
||||
newHost.keyType !== existingHost.keyType ||
|
||||
newHost.credentialId !== existingHost.credentialId ||
|
||||
newHost.defaultPath !== existingHost.defaultPath ||
|
||||
JSON.stringify(newHost.tags) !==
|
||||
JSON.stringify(existingHost.tags) ||
|
||||
@@ -247,6 +249,10 @@ export function LeftSidebar({
|
||||
return () => clearTimeout(handler);
|
||||
}, [search]);
|
||||
|
||||
React.useEffect(() => {
|
||||
localStorage.setItem("leftSidebarOpen", JSON.stringify(isSidebarOpen));
|
||||
}, [isSidebarOpen]);
|
||||
|
||||
const filteredHosts = React.useMemo(() => {
|
||||
if (!debouncedSearch.trim()) return hosts;
|
||||
const q = debouncedSearch.trim().toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user
The
set_real_ip_from 127.0.0.1;directive is quite restrictive. If this NGINX instance is running inside a Docker container and is fronted by another proxy (e.g., another container in the same Docker network), the request will likely come from an internal Docker IP, not127.0.0.1. This would result in the real client IP not being correctly identified.To make this more robust, consider trusting the common Docker network ranges. This will cover most containerized and local proxy setups. You might want to adjust the CIDR blocks based on your specific Docker network configuration.