From 42c5682243c9c4d0d472d859d8ca8918c46e12f5 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Thu, 8 Oct 2026 05:37:58 +0800 Subject: [PATCH] fix(hosts): display protocol credential usernames for non-SSH hosts (#1572) --- .../hosts/protocol-auth/protocol-auth.ts | 15 ++++++- .../plugins/protocol-auth-fixture.test.ts | 17 ++++++++ src/ui/sidebar/tree/HostItem/HostItem.tsx | 20 ++++++++-- .../sidebar/tree/HostItem/HostItem.test.tsx | 40 +++++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/backend/hosts/protocol-auth/protocol-auth.ts b/src/backend/hosts/protocol-auth/protocol-auth.ts index f0c845c17..cd8963bf2 100644 --- a/src/backend/hosts/protocol-auth/protocol-auth.ts +++ b/src/backend/hosts/protocol-auth/protocol-auth.ts @@ -434,12 +434,25 @@ export async function loadProtocolAuthSummaries( const rows = await createCurrentHostProtocolAuthRepository().listRowsForHosts(ids); const keys = new Map(); + const usernames = new Map(); for (const row of rows) { if (!keys.has(row.userId)) { keys.set(row.userId, DataCrypto.getUserDataKey(row.userId)); } const own = result.get(row.hostId) ?? {}; - own[row.protocol] = summarize(row, keys.get(row.userId) ?? null); + const summary = summarize(row, keys.get(row.userId) ?? null); + if (row.authType === "credential" && row.credentialId) { + const key = `${row.userId}:${row.credentialId}`; + if (!usernames.has(key)) { + const credential = await findUsableCredential( + row.credentialId, + row.userId, + ); + usernames.set(key, credential?.username || null); + } + summary.username = usernames.get(key) ?? null; + } + own[row.protocol] = summary; result.set(row.hostId, own); } return result; diff --git a/src/backend/tests/plugins/protocol-auth-fixture.test.ts b/src/backend/tests/plugins/protocol-auth-fixture.test.ts index 5892a8631..96380aac0 100644 --- a/src/backend/tests/plugins/protocol-auth-fixture.test.ts +++ b/src/backend/tests/plugins/protocol-auth-fixture.test.ts @@ -329,6 +329,23 @@ describe("a plugin protocol core does not know", () => { }); }); + it("summarizes the usable credential username without exposing its secret", async () => { + const patch = readProtocolAuthPayload({ + protocolAuth: { spice: { authType: "credential", credentialId: 6 } }, + }); + await writeProtocolAuth("owner", HOST_ID, patch!, { isOwner: true }); + const summaries = await loadProtocolAuthSummaries([{ id: HOST_ID }]); + expect(summaries.get(HOST_ID)?.spice.username).toBe("cred-user"); + expect(JSON.stringify(summaries.get(HOST_ID))).not.toContain("cred-pass"); + const recipient = sanitizeHostForRecipient( + { protocolAuth: summaries.get(HOST_ID) }, + "connect", + ); + expect(recipient.protocolAuth).toEqual({ + spice: { authType: "credential" }, + }); + }); + it("lets a shared editor change the login but not its credential", async () => { await storeLogin(); const rename = readProtocolAuthPayload({ diff --git a/src/ui/sidebar/tree/HostItem/HostItem.tsx b/src/ui/sidebar/tree/HostItem/HostItem.tsx index df0e3ad4c..53177d2c4 100644 --- a/src/ui/sidebar/tree/HostItem/HostItem.tsx +++ b/src/ui/sidebar/tree/HostItem/HostItem.tsx @@ -148,10 +148,19 @@ const DOUBLE_CLICK_WINDOW_MS = 250; const DEFAULT_ROW_FIELDS = defaultHostRowFields(); export function formatHostAddress( - host: Pick, + host: Pick & + Partial>, fields: Pick, ): string { - const user = fields.showUsername && host.username ? `${host.username}@` : ""; + const protocol = + host.enableSsh === false ? enabledHostProtocols(host)[0] : undefined; + const username = + host.enableSsh === false + ? protocol + ? host.protocolAuth?.[protocol.id]?.username + : undefined + : host.username; + const user = fields.showUsername && username ? `${username}@` : ""; const port = fields.showPort && host.port ? `:${host.port}` : ""; return `${user}${host.ip}${port}`; } @@ -787,7 +796,12 @@ export function HostItem({ { e.stopPropagation(); - writeClipboardText(`${host.username}@${host.ip}`); + writeClipboardText( + formatHostAddress(host, { + showUsername: true, + showPort: false, + }), + ); toast.success(t("hosts.copiedToClipboard")); }} > diff --git a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx index 7b510da73..16fd83424 100644 --- a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx +++ b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx @@ -1,3 +1,4 @@ +import { registerHostProtocol } from "@/sidebar/host-protocols"; import { act, cleanup, @@ -402,3 +403,42 @@ describe("HostItem row fields", () => { ).toBe("root@10.0.0.5"); }); }); + +it("uses the enabled protocol username for non-SSH hosts", () => { + const dispose = registerHostProtocol({ + id: "rdp", + pluginId: "remote-desktop", + settingKey: "enableRdp", + defaultPort: 3389, + titleKey: "rdp", + icon: () => null, + }); + try { + const host: Host = { + ...baseHost, + enableSsh: false, + pluginSettings: { "remote-desktop": { enableRdp: true } }, + protocolAuth: { + rdp: { + authType: "credential", + credentialId: 6, + username: "Administrator", + fields: {}, + }, + }, + }; + const fields = { showUsername: true, showPort: false }; + expect(formatHostAddress(host, fields)).toBe("Administrator@10.0.0.5"); + expect(formatHostAddress({ ...host, enableSsh: true }, fields)).toBe( + "root@10.0.0.5", + ); + expect(formatHostAddress({ ...host, protocolAuth: {} }, fields)).toBe( + "10.0.0.5", + ); + expect(formatHostAddress(host, { ...fields, showUsername: false })).toBe( + "10.0.0.5", + ); + } finally { + dispose(); + } +});