diff --git a/backend/internal/web/dist/index.html b/backend/internal/web/dist/index.html index 2c645a3..aff6bf2 100644 --- a/backend/internal/web/dist/index.html +++ b/backend/internal/web/dist/index.html @@ -1,3 +1,3 @@ - - + +
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index acd0bd9..e4dceb1 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -340,7 +340,7 @@ function Dashboard() { {accounts.map((x) => ( ))} @@ -364,7 +364,9 @@ function Dashboard() {
Codex Account - {d.displayName} · {d.account.email || "尚未连接"} + {d.displayName} · {d.account.email + ? maskEmail(d.account.email) + : "尚未连接"}
{planLabel(d.account.planType)} @@ -437,10 +439,6 @@ function LimitCard({ x }: { x: Limit }) { -
- 已使用 {usedLabel}% - 剩余 {leftLabel}% -
); } @@ -812,7 +810,7 @@ function CodexSettings() { }} />
- {x.email || "尚未登录"} + {x.email ? maskEmail(x.email) : "尚未登录"} {planLabel(x.planType)} {validationLabel(x)} @@ -1156,6 +1154,22 @@ const duration = (v?: number) => : v < 3600 ? `${Math.floor(v / 60)} 分 ${v % 60} 秒` : `${(v / 3600).toFixed(1)} 小时`; +const maskEmailPart = (part: string) => { + if (part.length <= 1) return "*"; + if (part.length === 2) return part[0] + "*"; + return part[0] + "*".repeat(part.length - 2) + part.at(-1); +}; +const maskEmail = (email: string) => { + const at = email.lastIndexOf("@"); + if (at < 1 || at === email.length - 1) return maskEmailPart(email); + const local = email.slice(0, at); + const domain = email.slice(at + 1).split("."); + return `${maskEmailPart(local)}@${domain + .map((part, index) => + index === domain.length - 1 ? part : maskEmailPart(part), + ) + .join(".")}`; +}; const planLabel = (plan?: string) => { if (!plan) return "未识别套餐"; const labels: Record = { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 2a6f813..ae619d5 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -295,10 +295,10 @@ header p { gap: 18px; } .limit { - padding: 24px; + padding: 20px; display: grid; grid-template-columns: 1fr auto; - gap: 16px; + gap: 12px; align-items: start; } .limit > small { @@ -344,18 +344,6 @@ header p { .bar em { background: var(--green); } -.limit footer { - grid-column: 1/-1; - border-top: 1px solid var(--border); - padding-top: 14px; - display: flex; - gap: 18px; - color: var(--muted); - font-size: 12px; -} -.limit footer span:last-child { - margin-left: auto; -} .green { color: var(--green); } @@ -682,8 +670,7 @@ a { text-align: left; } .limit > small, - .bar, - .limit footer { + .bar { grid-column: 1; } } diff --git a/frontend/tests/settings-layout.spec.ts b/frontend/tests/settings-layout.spec.ts index 0d9ab8b..6e3900e 100644 --- a/frontend/tests/settings-layout.spec.ts +++ b/frontend/tests/settings-layout.spec.ts @@ -169,3 +169,53 @@ test("Codex account cards fit within the viewport", async ({ page }) => { expect(overflow.documentWidth).toBeLessThanOrEqual(overflow.viewportWidth); expect(overflow.cardWidth).toBeLessThanOrEqual(overflow.contentWidth); }); + +test("Codex account emails are masked everywhere they are displayed", async ({ + page, +}) => { + await page.route("**/api/v1/**", async (route) => { + const key = new URL(route.request().url()).pathname.replace("/api/v1/", ""); + if (key === "dashboard") + return route.fulfill({ + json: { + accountId: 1, + displayName: "默认账号", + account: { + email: "test@example.com", + planType: "plus", + connected: true, + }, + limits: [ + { + limitId: "primary", + windowType: "5h", + usedPercent: 25, + windowDurationMinutes: 300, + resetsAt: 0, + }, + ], + summary: {}, + usage: [], + fetchedAt: 0, + stale: false, + }, + }); + return route.fulfill({ json: responses[key] ?? {} }); + }); + + await page.goto("/"); + await expect(page.locator(".account-select")).toContainText("t**t@e*****e.com"); + await expect(page.locator(".account b")).toContainText("t**t@e*****e.com"); + await expect(page.getByText("已使用 25%", { exact: true })).toHaveCount(0); + await expect(page.getByText("剩余 75%", { exact: true })).toHaveCount(0); + await expect(page.locator(".bar")).toHaveAttribute( + "aria-label", + "5h:已使用 25%,剩余 75%", + ); + await expect(page.locator("body")).not.toContainText("test@example.com"); + + await page.goto("/settings"); + await page.getByRole("tab", { name: "Codex" }).click(); + await expect(page.locator(".account-meta").first()).toContainText("t**t@e*****e.com"); + await expect(page.locator("body")).not.toContainText("test@example.com"); +});