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
This commit is contained in:
Hyeonsang Cho 2026-10-25 11:09:49 +09:00 committed by GitHub
parent ec87cb9a21
commit 1792e4e432
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 122 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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" }]),

View File

@ -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", () => {