feat: add copy password button and fixed new line carriage issues and backend crash for auth key

This commit is contained in:
LukeGus
2026-01-15 01:40:02 -06:00
parent b7bd1e50b3
commit cb478477e9
7 changed files with 158 additions and 5 deletions

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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;

View File

@@ -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) {

View File

@@ -517,6 +517,7 @@ export function TopNavbar({
disableClose={disableClose}
isDragging={isDraggingThisTab}
isDragOver={false}
hostConfig={tab.hostConfig}
/>
</div>
);

View File

@@ -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"

View File

@@ -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,