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 }) {
-
);
}
@@ -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");
+});