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 { DataCrypto } from "../../utils/data-crypto.js";
|
||||||
import { SystemCrypto } from "../../utils/system-crypto.js";
|
import { SystemCrypto } from "../../utils/system-crypto.js";
|
||||||
import { DatabaseSaveTrigger } from "../db/index.js";
|
import { DatabaseSaveTrigger } from "../db/index.js";
|
||||||
|
import { parseSSHKey } from "../../utils/ssh-key-utils.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -396,6 +397,39 @@ router.post(
|
|||||||
sshDataObj.key_password = null;
|
sshDataObj.key_password = null;
|
||||||
sshDataObj.keyType = null;
|
sshDataObj.keyType = null;
|
||||||
} else if (effectiveAuthType === "key") {
|
} 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 = key || null;
|
||||||
sshDataObj.key_password = keyPassword || null;
|
sshDataObj.key_password = keyPassword || null;
|
||||||
sshDataObj.keyType = keyType;
|
sshDataObj.keyType = keyType;
|
||||||
@@ -685,7 +719,40 @@ router.put(
|
|||||||
sshDataObj.key_password = null;
|
sshDataObj.key_password = null;
|
||||||
sshDataObj.keyType = null;
|
sshDataObj.keyType = null;
|
||||||
} else if (effectiveAuthType === "key") {
|
} 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;
|
sshDataObj.key = key;
|
||||||
}
|
}
|
||||||
if (keyPassword !== undefined) {
|
if (keyPassword !== undefined) {
|
||||||
|
|||||||
@@ -492,7 +492,12 @@
|
|||||||
"hostManager": "Host Manager",
|
"hostManager": "Host Manager",
|
||||||
"cannotSplitTab": "Cannot split this tab",
|
"cannotSplitTab": "Cannot split this tab",
|
||||||
"tabNavigation": "Tab Navigation",
|
"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": {
|
"admin": {
|
||||||
"title": "Admin Settings",
|
"title": "Admin Settings",
|
||||||
@@ -1381,6 +1386,7 @@
|
|||||||
"enterSudoPassword": "Enter sudo password to continue this operation",
|
"enterSudoPassword": "Enter sudo password to continue this operation",
|
||||||
"sudoPassword": "Sudo password",
|
"sudoPassword": "Sudo password",
|
||||||
"sudoOperationFailed": "Sudo operation failed",
|
"sudoOperationFailed": "Sudo operation failed",
|
||||||
|
"sudoAuthFailed": "Sudo authentication failed",
|
||||||
"deleteOperation": "Delete files/folders",
|
"deleteOperation": "Delete files/folders",
|
||||||
"dragFilesToUpload": "Drop files here to upload",
|
"dragFilesToUpload": "Drop files here to upload",
|
||||||
"emptyFolder": "This folder is empty",
|
"emptyFolder": "This folder is empty",
|
||||||
|
|||||||
@@ -1164,7 +1164,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
fontSize: config.fontSize,
|
fontSize: config.fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
convertEol: true,
|
convertEol: false,
|
||||||
windowsMode: false,
|
windowsMode: false,
|
||||||
macOptionIsMeta: false,
|
macOptionIsMeta: false,
|
||||||
macOptionClickForcesSelection: false,
|
macOptionClickForcesSelection: false,
|
||||||
@@ -1360,7 +1360,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
const ctrlCode = key.charCodeAt(0) - 96;
|
const ctrlCode = key.charCodeAt(0) - 96;
|
||||||
if (webSocketRef.current?.readyState === 1) {
|
if (webSocketRef.current?.readyState === 1) {
|
||||||
webSocketRef.current.send(
|
webSocketRef.current.send(
|
||||||
JSON.stringify({ type: "input", data: String.fromCharCode(ctrlCode) }),
|
JSON.stringify({
|
||||||
|
type: "input",
|
||||||
|
data: String.fromCharCode(ctrlCode),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -113,10 +113,22 @@ export function HostManager({
|
|||||||
if (activeTab === "add_host" && value !== "add_host") {
|
if (activeTab === "add_host" && value !== "add_host") {
|
||||||
setEditingHost(null);
|
setEditingHost(null);
|
||||||
lastProcessedHostIdRef.current = undefined;
|
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") {
|
if (activeTab === "add_credential" && value !== "add_credential") {
|
||||||
setEditingCredential(null);
|
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);
|
setActiveTab(value);
|
||||||
|
|
||||||
if (updateTab && currentTabId !== undefined) {
|
if (updateTab && currentTabId !== undefined) {
|
||||||
|
|||||||
@@ -517,6 +517,7 @@ export function TopNavbar({
|
|||||||
disableClose={disableClose}
|
disableClose={disableClose}
|
||||||
isDragging={isDraggingThisTab}
|
isDragging={isDraggingThisTab}
|
||||||
isDragOver={false}
|
isDragOver={false}
|
||||||
|
hostConfig={tab.hostConfig}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import {
|
|||||||
Network,
|
Network,
|
||||||
ArrowDownUp as TunnelIcon,
|
ArrowDownUp as TunnelIcon,
|
||||||
Container as DockerIcon,
|
Container as DockerIcon,
|
||||||
|
Key,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import type { SSHHost } from "@/types";
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
tabType: string;
|
tabType: string;
|
||||||
@@ -32,6 +34,7 @@ interface TabProps {
|
|||||||
isDragOver?: boolean;
|
isDragOver?: boolean;
|
||||||
isValidDropTarget?: boolean;
|
isValidDropTarget?: boolean;
|
||||||
isHoveredDropTarget?: boolean;
|
isHoveredDropTarget?: boolean;
|
||||||
|
hostConfig?: SSHHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tab({
|
export function Tab({
|
||||||
@@ -51,9 +54,58 @@ export function Tab({
|
|||||||
isDragOver = false,
|
isDragOver = false,
|
||||||
isValidDropTarget = false,
|
isValidDropTarget = false,
|
||||||
isHoveredDropTarget = false,
|
isHoveredDropTarget = false,
|
||||||
|
hostConfig,
|
||||||
}: TabProps): React.ReactElement {
|
}: TabProps): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
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(
|
const tabBaseClasses = cn(
|
||||||
"relative flex items-center gap-1.5 px-3 w-full min-w-0",
|
"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",
|
"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>}
|
{suffix && <span className="text-sm flex-shrink-0">{suffix}</span>}
|
||||||
</div>
|
</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 && (
|
{canSplit && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
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',
|
'"Caskaydia Cove Nerd Font Mono", "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
theme: themeColors,
|
theme: themeColors,
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
convertEol: true,
|
convertEol: false,
|
||||||
windowsMode: false,
|
windowsMode: false,
|
||||||
macOptionIsMeta: false,
|
macOptionIsMeta: false,
|
||||||
macOptionClickForcesSelection: false,
|
macOptionClickForcesSelection: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user