mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 13:40:37 +00:00
* feat(authz): admin user-management UI and disabled-account copy (#4063 gap 3) Frontend slice of the account lifecycle backend (#6566): - Settings gains an admin-only Users page (core/auth/admin-users.ts -> GET/PATCH /api/v1/admin/users): email, read-only role, disabled state, and a suspend/restore toggle per row; the Gateway's rejection detail (last-active-admin 409) renders verbatim. The nav entry is hidden for non-admins and the page guards independently; ?settings=users joins the deep-link whitelist (now an exhaustive Record over SettingsSection). - A session that turns out disabled maps account_disabled to a login redirect that states the reason on all three surfaces: the SSR AuthResult tag (consumed exhaustively by the workspace/artifacts/auth layouts), the shared fetcher's 401 interception, and AuthProvider.refreshUser. No next target is preserved - a disabled account cannot sign back in. Login failures surface the localized account_disabled copy when the code is present. - userSchema stays disabled-optional (absent = not disabled); zh/en locales and docs updated in the same change. * fix(authz): address review on admin user UI and disabled copy (#4063 gap 3) Review follow-ups on PR #6641: - Direct /login visits with a suspended session now surface the SSR account_disabled verdict through AuthDisabledContext instead of discarding it (willem-bd): the auth layout wraps pages with the provider and the login form seeds its reason from it — no redirect, so no loop. Regression tests cover both verdict states. - admin-users.ts parses both endpoints against userSchema / z.array(userSchema) (zhfeng): malformed payloads fail the load instead of rendering phantom rows; toggle error copy is scoped to GatewayApiError with a localized fallback for anything else. - The localized account_disabled copy is login-only (zhfeng): sign-up keeps the backend message verbatim so a suspended account's email cannot be probed through the register surface. - Settings page folds the load banner and toggle error into a single alert region (zhfeng); adds coded-envelope 409, malformed-row, and load-failure tests; fetcher.ts documents the 401 body-consumption contract. - prettier across touched files (CI format gate). The admin-users query key keeps user?.id, matching the model-settings-page precedent (converge later per review).
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, rs } from "@rstest/core";
|
|
|
|
import { fetch as apiFetch } from "@/core/api/fetcher";
|
|
|
|
describe("api fetcher unauthorized redirect", () => {
|
|
let originalFetch: typeof globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
originalFetch = globalThis.fetch;
|
|
globalThis.fetch = rs.fn(
|
|
async () => new Response("", { status: 401 }),
|
|
) as unknown as typeof globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it("returns the caller to the full URL, query string included", async () => {
|
|
window.history.replaceState(
|
|
{},
|
|
"",
|
|
"/artifacts/view?path=%2Fmnt%2Fuser-data%2Foutputs%2Freport.md&thread_id=t-1",
|
|
);
|
|
|
|
// The wrapper redirects and then throws UnauthorizedError; the redirect
|
|
// target is what this test is about.
|
|
await expect(
|
|
apiFetch("/api/threads/t-1/artifacts/mnt/user-data/outputs/report.md"),
|
|
).rejects.toThrow();
|
|
|
|
expect(window.location.href).toContain("/login?next=");
|
|
const next = new URL(
|
|
window.location.href,
|
|
"http://localhost",
|
|
).searchParams.get("next");
|
|
expect(next).toBe(
|
|
"/artifacts/view?path=%2Fmnt%2Fuser-data%2Foutputs%2Freport.md&thread_id=t-1",
|
|
);
|
|
});
|
|
|
|
it("redirects an account_disabled 401 to a login page that states the reason", async () => {
|
|
globalThis.fetch = rs.fn(
|
|
async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
detail: { code: "account_disabled", message: "Account disabled" },
|
|
}),
|
|
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
) as unknown as typeof globalThis.fetch;
|
|
window.history.replaceState({}, "", "/workspace");
|
|
|
|
await expect(apiFetch("/api/v1/auth/me")).rejects.toThrow();
|
|
|
|
expect(window.location.href).toContain("/login?error=account_disabled");
|
|
// A disabled account cannot sign back in, so no return target is kept.
|
|
expect(window.location.href).not.toContain("next=");
|
|
});
|
|
});
|