mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 05:30:39 +00:00
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:
parent
ec87cb9a21
commit
1792e4e432
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" }]),
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user