diff --git a/app/(dashboard)/account/page.tsx b/app/(dashboard)/account/page.tsx index bbe80904..d96c5494 100644 --- a/app/(dashboard)/account/page.tsx +++ b/app/(dashboard)/account/page.tsx @@ -11,6 +11,7 @@ import { Skeleton } from "@/components/ui/skeleton" import { Page } from "@/components/page" import { PageHeader } from "@/components/page-header" import { useAccount, type AccountInfo } from "@/hooks/use-account" +import { resolveAccountDisplayName } from "@/lib/account-display" import { buildRoute } from "@/lib/routes" export default function AccountPage() { @@ -104,7 +105,7 @@ export default function AccountPage() { are facts to read, not objects to select. */}
{t("Username")}
-
{info.access_key}
+
{resolveAccountDisplayName(info)}
{t("Role")}
diff --git a/components/user/dropdown.tsx b/components/user/dropdown.tsx index 9f1929e6..32b82a5e 100644 --- a/components/user/dropdown.tsx +++ b/components/user/dropdown.tsx @@ -21,6 +21,7 @@ import { import { useAuth } from "@/contexts/auth-context" import { usePermissions } from "@/hooks/use-permissions" import { useSidebar } from "@/components/ui/sidebar" +import { resolveAccountDisplayName } from "@/lib/account-display" import { getThemeManifest } from "@/lib/theme/manifest" function resolveAvatarPath(path: string): string { @@ -68,7 +69,12 @@ export function UserDropdown() { } } - const accountName = (userInfo as { account_name?: string })?.account_name ?? "" + const accountIdentity = userInfo as { account_name?: string; username?: string; email?: string } | null + const accountName = resolveAccountDisplayName({ + access_key: accountIdentity?.account_name ?? "", + username: accountIdentity?.username, + email: accountIdentity?.email, + }) const roleLabel = isAdmin ? t("Administrator") : t("User") return ( diff --git a/hooks/use-account.ts b/hooks/use-account.ts index 0f0eccbe..a2378669 100644 --- a/hooks/use-account.ts +++ b/hooks/use-account.ts @@ -36,6 +36,8 @@ export interface AccountMfaSummary { */ export interface AccountInfo { access_key: string + username?: string + email?: string identity_type: IdentityType session_access_key?: string is_admin: boolean diff --git a/lib/account-display.ts b/lib/account-display.ts new file mode 100644 index 00000000..fabf84af --- /dev/null +++ b/lib/account-display.ts @@ -0,0 +1,14 @@ +export interface AccountDisplayIdentity { + access_key: string + username?: string + email?: string +} + +/** Resolve display-only identity metadata without changing the account principal. */ +export function resolveAccountDisplayName(identity: AccountDisplayIdentity): string { + return ( + [identity.username, identity.email, identity.access_key] + .find((value): value is string => typeof value === "string" && value.trim().length > 0) + ?.trim() ?? "" + ) +} diff --git a/tests/lib/account-display.test.ts b/tests/lib/account-display.test.ts new file mode 100644 index 00000000..07c6317d --- /dev/null +++ b/tests/lib/account-display.test.ts @@ -0,0 +1,25 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { resolveAccountDisplayName } from "../../lib/account-display" + +test("account display name prefers the configured OIDC username", () => { + assert.equal( + resolveAccountDisplayName({ + access_key: "virtual-parent", + username: "j.bruijns@pay.nl", + email: "fallback@pay.nl", + }), + "j.bruijns@pay.nl", + ) +}) + +test("account display name falls back to email when username is absent or blank", () => { + assert.equal( + resolveAccountDisplayName({ access_key: "virtual-parent", username: " ", email: "fallback@pay.nl" }), + "fallback@pay.nl", + ) +}) + +test("account display name remains compatible with responses that only contain access_key", () => { + assert.equal(resolveAccountDisplayName({ access_key: "legacy-account" }), "legacy-account") +}) diff --git a/tests/lib/account-surface.test.js b/tests/lib/account-surface.test.js index a88156c2..4a05462d 100644 --- a/tests/lib/account-surface.test.js +++ b/tests/lib/account-surface.test.js @@ -31,6 +31,15 @@ test("the profile page distinguishes a failed read from an empty profile", () => assert.match(source, /
{ + const source = read("components/user/dropdown.tsx") + + assert.match(source, /account_name\?: string; username\?: string; email\?: string/) + assert.match(source, /resolveAccountDisplayName\(\{/) + assert.doesNotMatch(source, /getAccountInfo\(\)/) }) test("the profile page explains why a root identity cannot be edited here", () => { diff --git a/tests/lib/ui-layout-source.test.js b/tests/lib/ui-layout-source.test.js index e7b69a4d..07573be6 100644 --- a/tests/lib/ui-layout-source.test.js +++ b/tests/lib/ui-layout-source.test.js @@ -57,7 +57,8 @@ test("the account menu names the signed-in identity and its authority", () => { // A menu that opens on an avatar with no name answers neither "who am I" nor // "with what authority", which is the gap this menu existed with before. - assert.match(source, /const accountName = \(userInfo as \{ account_name\?: string \}\)\?\.account_name \?\? ""/) + assert.match(source, /const accountName = resolveAccountDisplayName\(\{/) + assert.match(source, /access_key: accountIdentity\?\.account_name \?\? ""/) assert.match(source, /const roleLabel = isAdmin \? t\("Administrator"\) : t\("User"\)/) assert.match(source, /\{accountName \|\| t\("Unknown user"\)\}/) assert.match(source, /t\("Profile"\)/)