Merge pull request #1431 from kaitranntt/codex/fix-privacy-mode-account-label-exposure

fix(ui): blur account identity badges in privacy mode
This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-30 16:12:09 -04:00
committed by GitHub
6 changed files with 170 additions and 7 deletions
@@ -172,6 +172,8 @@ export function AccountSurfaceCard({
const { t } = useTranslation();
const identity = getAccountIdentityPresentation(accountId, email, tokenFile);
const title = displayEmail || identity.email || accountId;
const sensitiveTitle = (value: string | null | undefined) =>
privacyMode ? undefined : (value ?? undefined);
const normalizedProvider = provider.toLowerCase();
const effectiveTier = resolveEffectiveTier(tier, quota);
const effectiveCodexBadge =
@@ -192,6 +194,7 @@ export function AccountSurfaceCard({
<span
className={cn(
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md shrink-0',
privacyMode && PRIVACY_BLUR_CLASS,
getTierBadgeClass(effectiveTier)
)}
>
@@ -201,9 +204,10 @@ export function AccountSurfaceCard({
{normalizedProvider === 'codex'
? effectiveCodexBadge?.label && (
<span
title={effectiveCodexBadge.label}
title={sensitiveTitle(effectiveCodexBadge.label)}
className={cn(
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md border shrink-0',
privacyMode && PRIVACY_BLUR_CLASS,
getCompactDetailBadgeClass(effectiveCodexBadge.audience)
)}
>
@@ -212,9 +216,10 @@ export function AccountSurfaceCard({
)
: identity.audienceLabel && (
<span
title={identity.audienceLabel}
title={sensitiveTitle(identity.audienceLabel)}
className={cn(
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md shrink-0',
privacyMode && PRIVACY_BLUR_CLASS,
getAudienceBadgeClass(identity.audience)
)}
>
@@ -223,9 +228,10 @@ export function AccountSurfaceCard({
)}
{normalizedProvider !== 'codex' && identity.compactDetailLabel && (
<span
title={identity.detailLabel ?? identity.compactDetailLabel}
title={sensitiveTitle(identity.detailLabel ?? identity.compactDetailLabel)}
className={cn(
'text-[8px] font-semibold px-1.5 py-0.5 rounded-md border shrink-0',
privacyMode && PRIVACY_BLUR_CLASS,
getCompactDetailBadgeClass(identity.audience)
)}
>
@@ -260,6 +266,7 @@ export function AccountSurfaceCard({
<span
className={cn(
'absolute -bottom-0.5 -right-0.5 text-[7px] font-bold uppercase px-1 py-px rounded ring-1 ring-background',
privacyMode && PRIVACY_BLUR_CLASS,
effectiveTier === 'ultra'
? 'bg-violet-500/20 text-violet-600 dark:bg-violet-500/30 dark:text-violet-300'
: 'bg-yellow-500/20 text-yellow-700 dark:bg-yellow-500/25 dark:text-yellow-400'
@@ -276,7 +283,7 @@ export function AccountSurfaceCard({
className={cn('flex items-center min-w-0', isCompact ? 'gap-1.5' : 'gap-2 flex-wrap')}
>
<span
title={title}
title={sensitiveTitle(title)}
className={cn(
isCompact
? 'flex-1 min-w-0 text-xs font-semibold tracking-tight truncate leading-none'
@@ -292,6 +299,7 @@ export function AccountSurfaceCard({
variant="outline"
className={cn(
'text-[10px] h-4 px-1.5 border-transparent',
privacyMode && PRIVACY_BLUR_CLASS,
getAudienceBadgeClass(effectiveCodexBadge.audience)
)}
>
@@ -303,6 +311,7 @@ export function AccountSurfaceCard({
variant="outline"
className={cn(
'text-[10px] h-4 px-1.5 border-transparent',
privacyMode && PRIVACY_BLUR_CLASS,
getAudienceBadgeClass(identity.audience)
)}
>
@@ -310,7 +319,10 @@ export function AccountSurfaceCard({
</Badge>
)}
{!isCompact && normalizedProvider !== 'codex' && identity.detailLabel && (
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
<Badge
variant="outline"
className={cn('text-[10px] h-4 px-1.5', privacyMode && PRIVACY_BLUR_CLASS)}
>
{identity.detailLabel}
</Badge>
)}
@@ -53,6 +53,7 @@ export function AccountStep({
variant="outline"
className={cn(
'text-[10px] h-4 px-1.5 border-transparent',
privacyMode && PRIVACY_BLUR_CLASS,
codexBadge.audience === 'business'
? 'bg-sky-500/12 text-sky-700 dark:text-sky-300'
: codexBadge.audience === 'free'
@@ -67,6 +68,7 @@ export function AccountStep({
variant="outline"
className={cn(
'text-[10px] h-4 px-1.5 border-transparent',
privacyMode && PRIVACY_BLUR_CLASS,
identity.audience === 'business'
? 'bg-sky-500/12 text-sky-700 dark:text-sky-300'
: identity.audience === 'free'
@@ -78,7 +80,10 @@ export function AccountStep({
</Badge>
) : null}
{!codexBadge?.label && identity.detailLabel && (
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
<Badge
variant="outline"
className={cn('text-[10px] h-4 px-1.5', privacyMode && PRIVACY_BLUR_CLASS)}
>
{identity.detailLabel}
</Badge>
)}
@@ -89,6 +89,7 @@ export function VariantStep({
variant="outline"
className={cn(
'text-[10px] h-4 px-1.5 border-transparent',
privacyMode && PRIVACY_BLUR_CLASS,
selectedCodexBadge.audience === 'business'
? 'bg-sky-500/12 text-sky-700 dark:text-sky-300'
: selectedCodexBadge.audience === 'free'
@@ -103,6 +104,7 @@ export function VariantStep({
variant="outline"
className={cn(
'text-[10px] h-4 px-1.5 border-transparent',
privacyMode && PRIVACY_BLUR_CLASS,
selectedAccountIdentity.audience === 'business'
? 'bg-sky-500/12 text-sky-700 dark:text-sky-300'
: selectedAccountIdentity.audience === 'free'
@@ -114,7 +116,10 @@ export function VariantStep({
</Badge>
) : null}
{!selectedCodexBadge?.label && selectedAccountIdentity?.detailLabel && (
<Badge variant="outline" className="text-[10px] h-4 px-1.5">
<Badge
variant="outline"
className={cn('text-[10px] h-4 px-1.5', privacyMode && PRIVACY_BLUR_CLASS)}
>
{selectedAccountIdentity.detailLabel}
</Badge>
)}
@@ -0,0 +1,48 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { AccountSurfaceCard } from '@/components/account/shared/account-surface-card';
describe('AccountSurfaceCard privacy titles', () => {
it('keeps sensitive account title attributes available outside privacy mode', () => {
const { container } = render(
<AccountSurfaceCard
mode="compact"
provider="gemini"
accountId="person@example.com#abcdef12-team"
email="person@example.com"
/>
);
expect(screen.getByText('person@example.com')).toHaveAttribute('title', 'person@example.com');
expect(container.querySelector('[title="Business"]')).toBeInTheDocument();
expect(container.querySelector('[title="Workspace abcdef12"]')).toBeInTheDocument();
});
it('removes sensitive account title attributes in privacy mode', () => {
const { container } = render(
<>
<AccountSurfaceCard
mode="compact"
provider="gemini"
accountId="person@example.com#abcdef12-team"
email="person@example.com"
privacyMode
/>
<AccountSurfaceCard
mode="compact"
provider="codex"
accountId="codex@example.com#enterprise-pro"
email="codex@example.com"
privacyMode
/>
</>
);
expect(screen.getByText('person@example.com')).not.toHaveAttribute('title');
expect(screen.getByText('codex@example.com')).not.toHaveAttribute('title');
expect(container.querySelector('[title="Business"]')).not.toBeInTheDocument();
expect(container.querySelector('[title="Workspace abcdef12"]')).not.toBeInTheDocument();
expect(container.querySelector('[title*="Enterprise"]')).not.toBeInTheDocument();
});
});
@@ -0,0 +1,55 @@
import { render, screen } from '@tests/setup/test-utils';
import { describe, expect, it, vi } from 'vitest';
import { AccountStep } from '@/components/setup/wizard/steps/account-step';
import { VariantStep } from '@/components/setup/wizard/steps/variant-step';
import type { OAuthAccount } from '@/lib/api-client';
const businessAccount: OAuthAccount = {
id: 'victim@example.com#04a0f049-team',
email: 'victim@example.com',
provider: 'gemini',
isDefault: false,
tokenFile: 'gemini-victim@example.com-04a0f049-team.json',
createdAt: '2026-01-01T00:00:00.000Z',
};
describe('setup account identity privacy', () => {
it('blurs account metadata badges in the account selection step', () => {
render(
<AccountStep
accounts={[businessAccount]}
privacyMode={true}
onSelect={vi.fn()}
onAddNew={vi.fn()}
onBack={vi.fn()}
/>
);
expect(screen.getByText('victim@example.com')).toHaveClass('blur-[4px]');
expect(screen.getByText('Business')).toHaveClass('blur-[4px]');
expect(screen.getByText('Workspace 04a0f049')).toHaveClass('blur-[4px]');
});
it('blurs account metadata badges in the variant step', () => {
render(
<VariantStep
selectedProvider="gemini"
catalog={{ models: [{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' }] }}
selectedAccount={businessAccount}
variantName="gemini"
modelName="gemini-2.5-pro"
isPending={false}
privacyMode={true}
onVariantNameChange={vi.fn()}
onModelChange={vi.fn()}
onBack={vi.fn()}
onSkip={vi.fn()}
onCreate={vi.fn()}
/>
);
expect(screen.getByText('victim@example.com')).toHaveClass('blur-[4px]');
expect(screen.getByText('Business')).toHaveClass('blur-[4px]');
expect(screen.getByText('Workspace 04a0f049')).toHaveClass('blur-[4px]');
});
});
@@ -146,4 +146,42 @@ describe('AccountSurfaceCard', () => {
expect(screen.queryByText('Pers')).not.toBeInTheDocument();
expect(screen.queryByTitle('Free')).not.toBeInTheDocument();
});
it('blurs detailed account identity metadata when privacy mode is enabled', () => {
render(
<AccountSurfaceCard
mode="detailed"
provider="gemini"
accountId="victim@example.com#04a0f049-team"
email="victim@example.com"
displayEmail="victim@example.com"
tokenFile="gemini-victim@example.com-04a0f049-team.json"
privacyMode={true}
showQuota={false}
/>
);
expect(screen.getByText('victim@example.com')).toHaveClass('blur-[4px]');
expect(screen.getByText('Business')).toHaveClass('blur-[4px]');
expect(screen.getByText('Workspace 04a0f049')).toHaveClass('blur-[4px]');
});
it('blurs compact account identity metadata when privacy mode is enabled', () => {
render(
<AccountSurfaceCard
mode="compact"
provider="gemini"
accountId="victim@example.com#04a0f049-team"
email="victim@example.com"
displayEmail="victim@example.com"
tokenFile="gemini-victim@example.com-04a0f049-team.json"
privacyMode={true}
showQuota={false}
/>
);
expect(screen.getByText('victim@example.com')).toHaveClass('blur-[4px]');
expect(screen.getByText('Biz')).toHaveClass('blur-[4px]');
expect(screen.getByText('04a0f049')).toHaveClass('blur-[4px]');
});
});