feat: add copy password button and fixed new line carriage issues and backend crash for auth key
This commit is contained in:
@@ -35,6 +35,7 @@ import { PermissionManager } from "../../utils/permission-manager.js";
|
||||
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||
import { SystemCrypto } from "../../utils/system-crypto.js";
|
||||
import { DatabaseSaveTrigger } from "../db/index.js";
|
||||
import { parseSSHKey } from "../../utils/ssh-key-utils.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -396,6 +397,39 @@ router.post(
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
} else if (effectiveAuthType === "key") {
|
||||
if (key && typeof key === "string") {
|
||||
if (!key.includes("-----BEGIN") || !key.includes("-----END")) {
|
||||
sshLogger.warn("Invalid SSH key format provided", {
|
||||
operation: "host_create",
|
||||
userId,
|
||||
name,
|
||||
ip,
|
||||
port,
|
||||
});
|
||||
return res.status(400).json({
|
||||
error: "Invalid SSH key format. Key must be in PEM format.",
|
||||
});
|
||||
}
|
||||
|
||||
const keyValidation = parseSSHKey(
|
||||
key,
|
||||
typeof keyPassword === "string" ? keyPassword : undefined,
|
||||
);
|
||||
if (!keyValidation.success) {
|
||||
sshLogger.warn("SSH key validation failed", {
|
||||
operation: "host_create",
|
||||
userId,
|
||||
name,
|
||||
ip,
|
||||
port,
|
||||
error: keyValidation.error,
|
||||
});
|
||||
return res.status(400).json({
|
||||
error: `Invalid SSH key: ${keyValidation.error || "Unable to parse key"}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sshDataObj.key = key || null;
|
||||
sshDataObj.key_password = keyPassword || null;
|
||||
sshDataObj.keyType = keyType;
|
||||
@@ -685,7 +719,40 @@ router.put(
|
||||
sshDataObj.key_password = null;
|
||||
sshDataObj.keyType = null;
|
||||
} else if (effectiveAuthType === "key") {
|
||||
if (key) {
|
||||
if (key && typeof key === "string") {
|
||||
if (!key.includes("-----BEGIN") || !key.includes("-----END")) {
|
||||
sshLogger.warn("Invalid SSH key format provided", {
|
||||
operation: "host_update",
|
||||
hostId: parseInt(hostId),
|
||||
userId,
|
||||
name,
|
||||
ip,
|
||||
port,
|
||||
});
|
||||
return res.status(400).json({
|
||||
error: "Invalid SSH key format. Key must be in PEM format.",
|
||||
});
|
||||
}
|
||||
|
||||
const keyValidation = parseSSHKey(
|
||||
key,
|
||||
typeof keyPassword === "string" ? keyPassword : undefined,
|
||||
);
|
||||
if (!keyValidation.success) {
|
||||
sshLogger.warn("SSH key validation failed", {
|
||||
operation: "host_update",
|
||||
hostId: parseInt(hostId),
|
||||
userId,
|
||||
name,
|
||||
ip,
|
||||
port,
|
||||
error: keyValidation.error,
|
||||
});
|
||||
return res.status(400).json({
|
||||
error: `Invalid SSH key: ${keyValidation.error || "Unable to parse key"}`,
|
||||
});
|
||||
}
|
||||
|
||||
sshDataObj.key = key;
|
||||
}
|
||||
if (keyPassword !== undefined) {
|
||||
|
||||
@@ -492,7 +492,12 @@
|
||||
"hostManager": "Host Manager",
|
||||
"cannotSplitTab": "Cannot split this tab",
|
||||
"tabNavigation": "Tab Navigation",
|
||||
"hostTabTitle": "{{username}}@{{ip}}:{{port}}"
|
||||
"hostTabTitle": "{{username}}@{{ip}}:{{port}}",
|
||||
"copyPassword": "Copy Password",
|
||||
"copySudoPassword": "Copy Sudo Password",
|
||||
"passwordCopied": "Password copied to clipboard",
|
||||
"sudoPasswordCopied": "Sudo password copied to clipboard",
|
||||
"noPasswordAvailable": "No password available"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin Settings",
|
||||
@@ -1381,6 +1386,7 @@
|
||||
"enterSudoPassword": "Enter sudo password to continue this operation",
|
||||
"sudoPassword": "Sudo password",
|
||||
"sudoOperationFailed": "Sudo operation failed",
|
||||
"sudoAuthFailed": "Sudo authentication failed",
|
||||
"deleteOperation": "Delete files/folders",
|
||||
"dragFilesToUpload": "Drop files here to upload",
|
||||
"emptyFolder": "This folder is empty",
|
||||
|
||||
@@ -1164,7 +1164,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
fontSize: config.fontSize,
|
||||
fontFamily,
|
||||
allowTransparency: true,
|
||||
convertEol: true,
|
||||
convertEol: false,
|
||||
windowsMode: false,
|
||||
macOptionIsMeta: false,
|
||||
macOptionClickForcesSelection: false,
|
||||
@@ -1360,7 +1360,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const ctrlCode = key.charCodeAt(0) - 96;
|
||||
if (webSocketRef.current?.readyState === 1) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({ type: "input", data: String.fromCharCode(ctrlCode) }),
|
||||
JSON.stringify({
|
||||
type: "input",
|
||||
data: String.fromCharCode(ctrlCode),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -113,10 +113,22 @@ export function HostManager({
|
||||
if (activeTab === "add_host" && value !== "add_host") {
|
||||
setEditingHost(null);
|
||||
lastProcessedHostIdRef.current = undefined;
|
||||
|
||||
// Clear hostConfig from tab data when leaving add_host tab
|
||||
if (updateTab && currentTabId !== undefined) {
|
||||
updateTab(currentTabId, { hostConfig: null });
|
||||
}
|
||||
}
|
||||
if (activeTab === "add_credential" && value !== "add_credential") {
|
||||
setEditingCredential(null);
|
||||
}
|
||||
|
||||
// Clear editing state when switching TO add_host tab (to ensure fresh state)
|
||||
if (value === "add_host" && activeTab !== "add_host") {
|
||||
setEditingHost(null);
|
||||
lastProcessedHostIdRef.current = undefined;
|
||||
}
|
||||
|
||||
setActiveTab(value);
|
||||
|
||||
if (updateTab && currentTabId !== undefined) {
|
||||
|
||||
@@ -517,6 +517,7 @@ export function TopNavbar({
|
||||
disableClose={disableClose}
|
||||
isDragging={isDraggingThisTab}
|
||||
isDragOver={false}
|
||||
hostConfig={tab.hostConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
Network,
|
||||
ArrowDownUp as TunnelIcon,
|
||||
Container as DockerIcon,
|
||||
Key,
|
||||
} from "lucide-react";
|
||||
import type { SSHHost } from "@/types";
|
||||
|
||||
interface TabProps {
|
||||
tabType: string;
|
||||
@@ -32,6 +34,7 @@ interface TabProps {
|
||||
isDragOver?: boolean;
|
||||
isValidDropTarget?: boolean;
|
||||
isHoveredDropTarget?: boolean;
|
||||
hostConfig?: SSHHost;
|
||||
}
|
||||
|
||||
export function Tab({
|
||||
@@ -51,9 +54,58 @@ export function Tab({
|
||||
isDragOver = false,
|
||||
isValidDropTarget = false,
|
||||
isHoveredDropTarget = false,
|
||||
hostConfig,
|
||||
}: TabProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleCopyPassword = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!hostConfig) return;
|
||||
|
||||
const hasSshPassword =
|
||||
hostConfig.authType === "password" && hostConfig.password;
|
||||
const hasSudoPassword = hostConfig.sudoPassword;
|
||||
|
||||
if (!hasSshPassword && !hasSudoPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let passwordToCopy = "";
|
||||
|
||||
if (hasSshPassword) {
|
||||
passwordToCopy = hostConfig.password || "";
|
||||
} else if (hasSudoPassword) {
|
||||
passwordToCopy = hostConfig.sudoPassword;
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(passwordToCopy);
|
||||
} catch {
|
||||
// Silent fail - clipboard copy errors are not critical
|
||||
}
|
||||
};
|
||||
|
||||
const hasPassword =
|
||||
hostConfig &&
|
||||
((hostConfig.authType === "password" && hostConfig.password) ||
|
||||
hostConfig.sudoPassword);
|
||||
|
||||
const getPasswordButtonTitle = () => {
|
||||
if (!hostConfig) return "";
|
||||
|
||||
const hasSshPassword =
|
||||
hostConfig.authType === "password" && hostConfig.password;
|
||||
const hasSudoPassword = hostConfig.sudoPassword;
|
||||
|
||||
if (hasSshPassword) {
|
||||
return t("nav.copyPassword");
|
||||
} else if (hasSudoPassword) {
|
||||
return t("nav.copySudoPassword");
|
||||
}
|
||||
return t("nav.noPasswordAvailable");
|
||||
};
|
||||
|
||||
const tabBaseClasses = cn(
|
||||
"relative flex items-center gap-1.5 px-3 w-full min-w-0",
|
||||
"rounded-t-lg border-t-2 border-l-2 border-r-2",
|
||||
@@ -176,6 +228,18 @@ export function Tab({
|
||||
{suffix && <span className="text-sm flex-shrink-0">{suffix}</span>}
|
||||
</div>
|
||||
|
||||
{hasPassword && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={handleCopyPassword}
|
||||
title={getPasswordButtonTitle()}
|
||||
>
|
||||
<Key className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canSplit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -284,7 +284,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
'"Caskaydia Cove Nerd Font Mono", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
theme: themeColors,
|
||||
allowTransparency: true,
|
||||
convertEol: true,
|
||||
convertEol: false,
|
||||
windowsMode: false,
|
||||
macOptionIsMeta: false,
|
||||
macOptionClickForcesSelection: false,
|
||||
|
||||
Reference in New Issue
Block a user