From 1792e4e432bbcd055cf46eb19333400ba0b6bee2 Mon Sep 17 00:00:00 2001 From: Hyeonsang Cho Date: Sun, 25 Oct 2026 11:09:49 +0900 Subject: [PATCH] fix(frontend): stop re-validating account emails the backend accepted (#7449) * fix(frontend): stop re-validating account emails the backend accepted userSchema checked email with zod's email(), which rejects many addresses pydantic EmailStr registers (a&b@example.com, a@b.c, Unicode). /auth/me for such an account parsed as malformed: SSR answered gateway_unavailable, the offline banner never recovered, and the admin user list failed for every administrator. The backend owns address validation; keep a non-empty string. * docs(changelog): reference #7449 for the account email schema fix --- CHANGELOG.md | 7 ++ CHANGELOG_zh.md | 6 ++ frontend/src/core/auth/types.ts | 7 +- .../unit/core/auth/admin-users.dom.test.ts | 19 +++++ frontend/tests/unit/core/auth/server.test.ts | 84 +++++++++++++++++++ 5 files changed, 122 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2864fce79..7f199e5517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1397,6 +1397,12 @@ As of 2026-10-21, the milestone includes **829 merged pull requests**. ### Fixed +- **frontend:** Accounts whose email the backend accepted but the frontend's + stricter pattern did not (for example `a&b@example.com`, `a@b.c` or a + Unicode address) now get the normal workspace. Their sessions used to show + the gateway-offline fallback, and one such account made the admin + **Settings → Users** list fail for every administrator. ([#7449]) + - **persistence:** A PostgreSQL bootstrap failure inside an existing database now reports its own error. The Gateway treated any error mentioning "does not exist", such as a missing column or relation during a migration, as a missing @@ -13069,3 +13075,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#7364]: https://github.com/bytedance/deer-flow/pull/7364 [#7400]: https://github.com/bytedance/deer-flow/pull/7400 [#7410]: https://github.com/bytedance/deer-flow/pull/7410 +[#7449]: https://github.com/bytedance/deer-flow/pull/7449 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index aeb750e6ae..5aaa119f29 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -1112,6 +1112,11 @@ ### 修复 +- **前端:** 后端接受、但前端更严格的格式检查不接受的邮箱账号(如 + `a&b@example.com`、`a@b.c` 或含 Unicode 的地址)现在可以正常进入工作区。此前这些 + 账号的会话会显示网关离线界面,且只要存在一个这样的账号,所有管理员的 + “设置 → 用户”列表都会加载失败。([#7449]) + - **持久化:** 在已存在的数据库中 PostgreSQL 启动初始化失败时,现在会报告原始错误。 此前 Gateway 会把任何包含 "does not exist" 的错误(例如迁移时缺少列或表)当作数据库 不存在并执行 `CREATE DATABASE`,启动随之以 `database ... already exists` 或 @@ -10432,3 +10437,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#7364]: https://github.com/bytedance/deer-flow/pull/7364 [#7400]: https://github.com/bytedance/deer-flow/pull/7400 [#7410]: https://github.com/bytedance/deer-flow/pull/7410 +[#7449]: https://github.com/bytedance/deer-flow/pull/7449 diff --git a/frontend/src/core/auth/types.ts b/frontend/src/core/auth/types.ts index 01af671649..3846f1ef91 100644 --- a/frontend/src/core/auth/types.ts +++ b/frontend/src/core/auth/types.ts @@ -4,7 +4,12 @@ import { z } from "zod"; export const userSchema = z.object({ id: z.string(), - email: z.string().email(), + // Plain string: the backend owns address validation (pydantic EmailStr at + // registration, the IdP for OIDC) and accepts forms zod's email() rejects, + // such as `a&b@example.com`, `a@b.c` and Unicode addresses. Re-checking them + // here turned those accounts' sessions into gateway_unavailable and failed + // the admin user list. Empty is invalid backend-side. + email: z.string().min(1), // Plain string, kept in lockstep with the backend User model (RFC #4063 // gap 2): custom roles configured in the authorization provider are // assignable via the admin API and arrive here verbatim. Empty is invalid diff --git a/frontend/tests/unit/core/auth/admin-users.dom.test.ts b/frontend/tests/unit/core/auth/admin-users.dom.test.ts index 86d0fc0ca6..55c981c470 100644 --- a/frontend/tests/unit/core/auth/admin-users.dom.test.ts +++ b/frontend/tests/unit/core/auth/admin-users.dom.test.ts @@ -128,6 +128,25 @@ describe("admin user management client", () => { }); }); + test("lists users whose addresses only the backend's validator accepts", async () => { + // One such account used to fail the whole list for every administrator. + const rows = ["a&b@example.com", "a@b.c", "josé@example.com"].map( + (email, index) => ({ + id: `u-${index}`, + email, + system_role: "user", + needs_setup: false, + oauth_provider: null, + disabled: false, + }), + ); + globalThis.fetch = rs.fn(async () => + jsonResponse(rows), + ) as unknown as typeof globalThis.fetch; + + await expect(loadAdminUsers()).resolves.toEqual(rows); + }); + test("a malformed row rejects instead of rendering a phantom user", async () => { globalThis.fetch = rs.fn(async () => jsonResponse([{ email: "no-id@example.com" }]), diff --git a/frontend/tests/unit/core/auth/server.test.ts b/frontend/tests/unit/core/auth/server.test.ts index 78549440d8..71538193fb 100644 --- a/frontend/tests/unit/core/auth/server.test.ts +++ b/frontend/tests/unit/core/auth/server.test.ts @@ -251,6 +251,90 @@ describe("getServerSideUser — account_disabled contract", () => { }); }); +// ── email contract ───────────────────────────────────────────────── + +// Each address is one the backend registers with EmailStr and returns from +// /auth/me as shown (IDN domains come back in Unicode). +const BACKEND_ACCEPTED_EMAILS = [ + "a&b@example.com", + "a@b.c", + "a~b@example.com", + "josé@example.com", + "user@exämple.com", +]; + +describe("getServerSideUser — accepts every address the backend accepts", () => { + let saved: EnvSnapshot; + + beforeEach(() => { + saved = snapshotEnv(); + setEnv("DEER_FLOW_AUTH_DISABLED", undefined); + setEnv("NEXT_PUBLIC_STATIC_WEBSITE_ONLY", undefined); + rs.doMock("next/headers", () => ({ + cookies: rs.fn(async () => ({ + get: (name: string) => + name === "access_token" ? { value: "stub-token" } : undefined, + })), + })); + }); + + afterEach(() => { + restoreEnv(saved); + rs.unstubAllGlobals(); + rs.doUnmock("next/headers"); + }); + + test.each(BACKEND_ACCEPTED_EMAILS)( + "authenticates a session for %s", + async (email) => { + const me = { + id: "u-1", + email, + system_role: "user", + needs_setup: false, + oauth_provider: null, + permissions: null, + }; + rs.stubGlobal( + "fetch", + rs.fn( + async () => + new Response(JSON.stringify(me), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ), + ); + + const { getServerSideUser } = await loadFreshServerAuth(); + + await expect(getServerSideUser()).resolves.toMatchObject({ + tag: "authenticated", + user: { email }, + }); + }, + ); +}); + +describe("userSchema email", () => { + test.each(BACKEND_ACCEPTED_EMAILS)("keeps %s verbatim", (email) => { + const parsed = userSchema.parse({ + id: "1", + email, + system_role: "user", + }); + + expect(parsed.email).toBe(email); + }); + + test("still rejects an empty or missing email", () => { + expect(() => + userSchema.parse({ id: "1", email: "", system_role: "user" }), + ).toThrow(); + expect(() => userSchema.parse({ id: "1", system_role: "user" })).toThrow(); + }); +}); + // ── system_role contract (RFC #4063 gap 2) ───────────────────────── describe("userSchema system_role", () => {