From 196e6582cddcf3660fbe63ba81f4e17d9ff41b0e Mon Sep 17 00:00:00 2001 From: Clouder0 Date: Thu, 21 May 2026 20:35:32 +0800 Subject: [PATCH 1/3] fix: return full self user list shape --- src/app/api/v1/resources/users/handlers.ts | 18 +++++++++------ tests/api/v1/users/users.test.ts | 23 +++++++++++++++----- tests/unit/api/v1/api-client-actions.test.ts | 9 ++++---- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/app/api/v1/resources/users/handlers.ts b/src/app/api/v1/resources/users/handlers.ts index 1f306367e..39dc25f5d 100644 --- a/src/app/api/v1/resources/users/handlers.ts +++ b/src/app/api/v1/resources/users/handlers.ts @@ -78,15 +78,19 @@ export async function listCurrentUser(c: Context): Promise { }); } const actions = await import("@/actions/users"); - const result = await callAction( - c, - actions.getUserById, - [currentUserId] as never[], - c.get("auth") - ); + const result = await callAction(c, actions.getUsers, [] as never[], c.get("auth")); if (!result.ok) return actionError(c, result); + const currentUser = result.data.find((user) => user.id === currentUserId); + if (!currentUser) { + return createProblemResponse({ + status: 404, + instance: new URL(c.req.url).pathname, + errorCode: "resource.not_found", + detail: "Current user was not found.", + }); + } return jsonResponse({ - items: [redactUserKeys(result.data)], + items: [redactUserKeys(currentUser)], pageInfo: { nextCursor: null, hasMore: false, diff --git a/tests/api/v1/users/users.test.ts b/tests/api/v1/users/users.test.ts index 782727443..2079d7a8a 100644 --- a/tests/api/v1/users/users.test.ts +++ b/tests/api/v1/users/users.test.ts @@ -161,6 +161,7 @@ describe("v1 users endpoints", () => { test("returns the current user from a read-tier self list endpoint", async () => { validateAuthTokenMock.mockResolvedValueOnce(userSession); + getUsersMock.mockResolvedValueOnce([user(9)]); const self = await callV1Route({ method: "GET", @@ -170,16 +171,24 @@ describe("v1 users endpoints", () => { expect(self.response.status).toBe(200); expect(self.json).toMatchObject({ - items: [{ id: 9, name: "user-9" }], + items: [ + { + id: 9, + name: "user-9", + keys: [{ id: 10, name: "default", maskedKey: "sk-...cret" }], + }, + ], pageInfo: { nextCursor: null, hasMore: false, limit: 1, }, }); + expect(Array.isArray(self.json.items[0].keys)).toBe(true); expect(JSON.stringify(self.json)).not.toContain("sk-user-secret"); - expect(getUserByIdMock).toHaveBeenCalledWith(9); - expect(getUsersMock).not.toHaveBeenCalled(); + expect(JSON.stringify(self.json)).not.toContain("fullKey"); + expect(getUsersMock).toHaveBeenCalledWith(); + expect(getUserByIdMock).not.toHaveBeenCalled(); expect(validateAuthTokenMock).toHaveBeenCalledWith("user-token", { allowReadOnlyAccess: true, }); @@ -196,12 +205,14 @@ describe("v1 users endpoints", () => { expect(self.response.status).toBe(200); expect(self.json).toMatchObject({ - items: [{ id: 1, name: "user-1" }], + items: [{ id: 1, name: "user-1", keys: [{ id: 10, name: "default" }] }], pageInfo: { nextCursor: null, hasMore: false, limit: 1 }, }); + expect(Array.isArray(self.json.items[0].keys)).toBe(true); expect(JSON.stringify(self.json)).not.toContain("user-250"); - expect(getUserByIdMock).toHaveBeenCalledWith(1); - expect(getUsersMock).not.toHaveBeenCalled(); + expect(JSON.stringify(self.json)).not.toContain("fullKey"); + expect(getUsersMock).toHaveBeenCalledWith(); + expect(getUserByIdMock).not.toHaveBeenCalled(); }); test("reads user detail from an id-capable action instead of the first list page", async () => { diff --git a/tests/unit/api/v1/api-client-actions.test.ts b/tests/unit/api/v1/api-client-actions.test.ts index af29a4708..7dff23d90 100644 --- a/tests/unit/api/v1/api-client-actions.test.ts +++ b/tests/unit/api/v1/api-client-actions.test.ts @@ -143,12 +143,13 @@ describe("v1 action compatibility client", () => { }) ) .mockResolvedValueOnce({ - users: [{ id: 9, name: "self" }], - nextCursor: null, - hasMore: false, + items: [{ id: 9, name: "self", keys: [{ id: 90, name: "default" }] }], + pageInfo: { nextCursor: null, hasMore: false }, }); - await expect(users.getUsers()).resolves.toEqual([{ id: 9, name: "self" }]); + await expect(users.getUsers()).resolves.toMatchObject([ + { id: 9, name: "self", keys: [{ id: 90, name: "default" }] }, + ]); expect(getMock).toHaveBeenNthCalledWith(1, "/api/v1/users"); expect(getMock).toHaveBeenNthCalledWith(2, "/api/v1/users:self"); From 8b4213626b6a1d90fe97d9ba22e8392fa7a7f0ca Mon Sep 17 00:00:00 2001 From: Clouder0 Date: Thu, 21 May 2026 22:26:46 +0800 Subject: [PATCH 2/3] fix: target self user display loading --- src/actions/users.ts | 300 ++++++++++-------- src/app/api/v1/resources/users/handlers.ts | 13 +- tests/api/v1/users/users.test.ts | 30 +- tests/unit/api/v1/api-client-actions.test.ts | 7 +- .../users-action-get-users-compat.test.ts | 58 ++++ 5 files changed, 270 insertions(+), 138 deletions(-) diff --git a/src/actions/users.ts b/src/actions/users.ts index 1ffbe2c3e..323f91131 100644 --- a/src/actions/users.ts +++ b/src/actions/users.ts @@ -185,6 +185,135 @@ function canExposeFullKey( return session.key.canLoginWebUi && (isAdmin || session.user.id === targetUser.id); } +async function buildUserDisplays( + users: User[], + session: UserActionSession, + isAdmin: boolean +): Promise { + if (users.length === 0) { + return []; + } + + const locale = await getLocale(); + const t = await getTranslations("users"); + const userIds = users.map((u) => u.id); + const [keysMap, usageMap] = await Promise.all([ + findKeyListBatch(userIds), + findKeyUsageTodayBatch(userIds), + ]); + const statisticsMap = await findKeysStatisticsBatchFromKeys(keysMap); + + return users.map((user) => { + try { + const keys = keysMap.get(user.id) || []; + const usageRecords = usageMap.get(user.id) || []; + const keyStatistics = statisticsMap.get(user.id) || []; + const canUserManageKey = canExposeFullKey(session, user, isAdmin); + + const usageLookup = new Map( + usageRecords.map((item) => [ + item.keyId, + { totalCost: item.totalCost ?? 0, totalTokens: item.totalTokens ?? 0 }, + ]) + ); + const statisticsLookup = new Map(keyStatistics.map((stat) => [stat.keyId, stat])); + + return { + id: user.id, + name: user.name, + note: user.description || undefined, + role: user.role, + rpm: user.rpm, + dailyQuota: user.dailyQuota, + providerGroup: user.providerGroup || undefined, + tags: user.tags || [], + limit5hUsd: user.limit5hUsd ?? null, + limit5hResetMode: user.limit5hResetMode, + limitWeeklyUsd: user.limitWeeklyUsd ?? null, + limitMonthlyUsd: user.limitMonthlyUsd ?? null, + limitTotalUsd: user.limitTotalUsd ?? null, + costResetAt: user.costResetAt ?? null, + limitConcurrentSessions: user.limitConcurrentSessions ?? null, + dailyResetMode: user.dailyResetMode, + dailyResetTime: user.dailyResetTime, + isEnabled: user.isEnabled, + expiresAt: user.expiresAt ?? null, + allowedClients: user.allowedClients || [], + blockedClients: user.blockedClients || [], + allowedModels: user.allowedModels ?? [], + keys: keys.map((key) => { + const stats = statisticsLookup.get(key.id); + return { + id: key.id, + name: key.name, + maskedKey: maskKey(key.key), + canReveal: canUserManageKey, + canCopy: canUserManageKey, + expiresAt: key.expiresAt + ? key.expiresAt.toISOString().split("T")[0] + : t("neverExpires"), + status: key.isEnabled ? "enabled" : ("disabled" as const), + createdAt: key.createdAt, + createdAtFormatted: key.createdAt.toLocaleString(locale, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }), + todayUsage: usageLookup.get(key.id)?.totalCost ?? 0, + todayTokens: usageLookup.get(key.id)?.totalTokens ?? 0, + todayCallCount: stats?.todayCallCount ?? 0, + lastUsedAt: stats?.lastUsedAt ?? null, + lastProviderName: stats?.lastProviderName ?? null, + modelStats: stats?.modelStats ?? [], + canLoginWebUi: key.canLoginWebUi, + limit5hUsd: key.limit5hUsd, + limit5hResetMode: key.limit5hResetMode, + limitDailyUsd: key.limitDailyUsd, + dailyResetMode: key.dailyResetMode, + dailyResetTime: key.dailyResetTime, + limitWeeklyUsd: key.limitWeeklyUsd, + limitMonthlyUsd: key.limitMonthlyUsd, + limitTotalUsd: key.limitTotalUsd, + limitConcurrentSessions: key.limitConcurrentSessions || 0, + costResetAt: key.costResetAt?.toISOString() ?? null, + providerGroup: key.providerGroup, + }; + }), + }; + } catch (error) { + logger.error(`Failed to process keys for user ${user.id}:`, error); + return { + id: user.id, + name: user.name, + note: user.description || undefined, + role: user.role, + rpm: user.rpm, + dailyQuota: user.dailyQuota, + providerGroup: user.providerGroup || undefined, + tags: user.tags || [], + limit5hUsd: user.limit5hUsd ?? null, + limit5hResetMode: user.limit5hResetMode, + limitWeeklyUsd: user.limitWeeklyUsd ?? null, + limitMonthlyUsd: user.limitMonthlyUsd ?? null, + limitTotalUsd: user.limitTotalUsd ?? null, + costResetAt: user.costResetAt ?? null, + limitConcurrentSessions: user.limitConcurrentSessions ?? null, + dailyResetMode: user.dailyResetMode, + dailyResetTime: user.dailyResetTime, + isEnabled: user.isEnabled, + expiresAt: user.expiresAt ?? null, + allowedClients: user.allowedClients || [], + blockedClients: user.blockedClients || [], + allowedModels: user.allowedModels ?? [], + keys: [], + }; + } + }); +} + /** * 批量获取用户列表的返回结果。 */ @@ -334,10 +463,6 @@ export async function getUsers(params?: GetUsersBatchParams): Promise u.id); - const [keysMap, usageMap] = await Promise.all([ - findKeyListBatch(userIds), - findKeyUsageTodayBatch(userIds), - ]); - const statisticsMap = await findKeysStatisticsBatchFromKeys(keysMap); + return await buildUserDisplays(users, session, isAdmin); + } catch (error) { + logger.error("Failed to fetch user data:", error); + return []; + } +} - const userDisplays: UserDisplay[] = users.map((user) => { - try { - const keys = keysMap.get(user.id) || []; - const usageRecords = usageMap.get(user.id) || []; - const keyStatistics = statisticsMap.get(user.id) || []; +/** + * 获取当前登录用户的用户管理页显示数据。 + * + * 与 getUsers() 的非管理员路径保持同一返回形状,但不让管理员 self endpoint + * 退化为全量用户列表扫描。 + */ +export async function getCurrentUserDisplay(): Promise> { + try { + const tError = await getTranslations("errors"); + const session = await getSession(); + if (!session) { + return { + ok: false, + error: tError("UNAUTHORIZED"), + errorCode: ERROR_CODES.UNAUTHORIZED, + }; + } - const usageLookup = new Map( - usageRecords.map((item) => [ - item.keyId, - { totalCost: item.totalCost ?? 0, totalTokens: item.totalTokens ?? 0 }, - ]) - ); - const statisticsLookup = new Map(keyStatistics.map((stat) => [stat.keyId, stat])); + const user = await findUserById(session.user.id); + if (!user) { + return { + ok: false, + error: tError("USER_NOT_FOUND"), + errorCode: ERROR_CODES.NOT_FOUND, + }; + } - return { - id: user.id, - name: user.name, - note: user.description || undefined, - role: user.role, - rpm: user.rpm, - dailyQuota: user.dailyQuota, - providerGroup: user.providerGroup || undefined, - tags: user.tags || [], - limit5hUsd: user.limit5hUsd ?? null, - limit5hResetMode: user.limit5hResetMode, - limitWeeklyUsd: user.limitWeeklyUsd ?? null, - limitMonthlyUsd: user.limitMonthlyUsd ?? null, - limitTotalUsd: user.limitTotalUsd ?? null, - costResetAt: user.costResetAt ?? null, - limitConcurrentSessions: user.limitConcurrentSessions ?? null, - dailyResetMode: user.dailyResetMode, - dailyResetTime: user.dailyResetTime, - isEnabled: user.isEnabled, - expiresAt: user.expiresAt ?? null, - allowedClients: user.allowedClients || [], - blockedClients: user.blockedClients || [], - allowedModels: user.allowedModels ?? [], - keys: keys.map((key) => { - const stats = statisticsLookup.get(key.id); - const canUserManageKey = canExposeFullKey(session, user, isAdmin); - return { - id: key.id, - name: key.name, - maskedKey: maskKey(key.key), - canReveal: canUserManageKey, - canCopy: canUserManageKey, - expiresAt: key.expiresAt - ? key.expiresAt.toISOString().split("T")[0] - : t("neverExpires"), - status: key.isEnabled ? "enabled" : ("disabled" as const), - createdAt: key.createdAt, - createdAtFormatted: key.createdAt.toLocaleString(locale, { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - todayUsage: usageLookup.get(key.id)?.totalCost ?? 0, - todayTokens: usageLookup.get(key.id)?.totalTokens ?? 0, - todayCallCount: stats?.todayCallCount ?? 0, - lastUsedAt: stats?.lastUsedAt ?? null, - lastProviderName: stats?.lastProviderName ?? null, - modelStats: stats?.modelStats ?? [], - // Web UI 登录权限控制 - canLoginWebUi: key.canLoginWebUi, - // 限额配置 - limit5hUsd: key.limit5hUsd, - limit5hResetMode: key.limit5hResetMode, - limitDailyUsd: key.limitDailyUsd, - dailyResetMode: key.dailyResetMode, - dailyResetTime: key.dailyResetTime, - limitWeeklyUsd: key.limitWeeklyUsd, - limitMonthlyUsd: key.limitMonthlyUsd, - limitTotalUsd: key.limitTotalUsd, - limitConcurrentSessions: key.limitConcurrentSessions || 0, - costResetAt: key.costResetAt?.toISOString() ?? null, - providerGroup: key.providerGroup, - }; - }), - }; - } catch (error) { - logger.error(`Failed to process keys for user ${user.id}:`, error); - return { - id: user.id, - name: user.name, - note: user.description || undefined, - role: user.role, - rpm: user.rpm, - dailyQuota: user.dailyQuota, - providerGroup: user.providerGroup || undefined, - tags: user.tags || [], - limit5hUsd: user.limit5hUsd ?? null, - limit5hResetMode: user.limit5hResetMode, - limitWeeklyUsd: user.limitWeeklyUsd ?? null, - limitMonthlyUsd: user.limitMonthlyUsd ?? null, - limitTotalUsd: user.limitTotalUsd ?? null, - costResetAt: user.costResetAt ?? null, - limitConcurrentSessions: user.limitConcurrentSessions ?? null, - dailyResetMode: user.dailyResetMode, - dailyResetTime: user.dailyResetTime, - isEnabled: user.isEnabled, - expiresAt: user.expiresAt ?? null, - allowedClients: user.allowedClients || [], - blockedClients: user.blockedClients || [], - allowedModels: user.allowedModels ?? [], - keys: [], - }; - } - }); + const [displayUser] = await buildUserDisplays([user], session, session.user.role === "admin"); + if (!displayUser) { + return { + ok: false, + error: tError("USER_NOT_FOUND"), + errorCode: ERROR_CODES.NOT_FOUND, + }; + } - return userDisplays; + return { ok: true, data: displayUser }; } catch (error) { - logger.error("Failed to fetch user data:", error); - return []; + logger.error("Failed to fetch current user display data:", error); + const message = + error instanceof Error ? error.message : "Failed to fetch current user display data"; + return { ok: false, error: message, errorCode: ERROR_CODES.INTERNAL_ERROR }; } } diff --git a/src/app/api/v1/resources/users/handlers.ts b/src/app/api/v1/resources/users/handlers.ts index 39dc25f5d..9216c90a4 100644 --- a/src/app/api/v1/resources/users/handlers.ts +++ b/src/app/api/v1/resources/users/handlers.ts @@ -78,10 +78,9 @@ export async function listCurrentUser(c: Context): Promise { }); } const actions = await import("@/actions/users"); - const result = await callAction(c, actions.getUsers, [] as never[], c.get("auth")); + const result = await callAction(c, actions.getCurrentUserDisplay, [] as never[], c.get("auth")); if (!result.ok) return actionError(c, result); - const currentUser = result.data.find((user) => user.id === currentUserId); - if (!currentUser) { + if (result.data.id !== currentUserId) { return createProblemResponse({ status: 404, instance: new URL(c.req.url).pathname, @@ -89,12 +88,13 @@ export async function listCurrentUser(c: Context): Promise { detail: "Current user was not found.", }); } + const items = [redactUserKeys(result.data)]; return jsonResponse({ - items: [redactUserKeys(currentUser)], + items, pageInfo: { nextCursor: null, hasMore: false, - limit: 1, + limit: items.length, }, }); } @@ -357,6 +357,9 @@ function redactUserKeys(value: unknown): unknown { if (!value || typeof value !== "object") { return value; } + if (value instanceof Date) { + return value; + } const entries = Object.entries(value as Record) .filter(([key]) => key !== "fullKey") diff --git a/tests/api/v1/users/users.test.ts b/tests/api/v1/users/users.test.ts index 2079d7a8a..4b80db618 100644 --- a/tests/api/v1/users/users.test.ts +++ b/tests/api/v1/users/users.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; const validateAuthTokenMock = vi.hoisted(() => vi.fn()); const getUsersMock = vi.hoisted(() => vi.fn()); +const getCurrentUserDisplayMock = vi.hoisted(() => vi.fn()); const getUserByIdMock = vi.hoisted(() => vi.fn()); const getUsersBatchCoreMock = vi.hoisted(() => vi.fn()); const getUsersUsageBatchMock = vi.hoisted(() => vi.fn()); @@ -29,6 +30,7 @@ vi.mock("@/lib/auth", async (importOriginal) => { vi.mock("@/actions/users", () => ({ getUsers: getUsersMock, + getCurrentUserDisplay: getCurrentUserDisplayMock, getUserById: getUserByIdMock, getUsersBatchCore: getUsersBatchCoreMock, getUsersUsageBatch: getUsersUsageBatchMock, @@ -84,6 +86,7 @@ describe("v1 users endpoints", () => { data: { users: [user(1)], nextCursor: "next", hasMore: true }, }); getUsersMock.mockResolvedValue([user(1), user(250)]); + getCurrentUserDisplayMock.mockResolvedValue({ ok: true, data: user(1) }); getUserByIdMock.mockImplementation(async (id: number) => { if (id === 404) { return { ok: false, error: "Not found", errorCode: "NOT_FOUND" }; @@ -161,7 +164,23 @@ describe("v1 users endpoints", () => { test("returns the current user from a read-tier self list endpoint", async () => { validateAuthTokenMock.mockResolvedValueOnce(userSession); - getUsersMock.mockResolvedValueOnce([user(9)]); + getCurrentUserDisplayMock.mockResolvedValueOnce({ + ok: true, + data: { + ...user(9), + expiresAt: new Date("2026-05-07T07:41:10.000Z"), + costResetAt: new Date("2026-04-30T00:00:00.000Z"), + keys: [ + { + id: 10, + name: "default", + maskedKey: "sk-...cret", + fullKey: "sk-user-secret", + createdAt: new Date("2026-04-30T07:41:10.000Z"), + }, + ], + }, + }); const self = await callV1Route({ method: "GET", @@ -185,9 +204,13 @@ describe("v1 users endpoints", () => { }, }); expect(Array.isArray(self.json.items[0].keys)).toBe(true); + expect(self.json.items[0].expiresAt).toBe("2026-05-07T07:41:10.000Z"); + expect(self.json.items[0].costResetAt).toBe("2026-04-30T00:00:00.000Z"); + expect(self.json.items[0].keys[0].createdAt).toBe("2026-04-30T07:41:10.000Z"); expect(JSON.stringify(self.json)).not.toContain("sk-user-secret"); expect(JSON.stringify(self.json)).not.toContain("fullKey"); - expect(getUsersMock).toHaveBeenCalledWith(); + expect(getCurrentUserDisplayMock).toHaveBeenCalledWith(); + expect(getUsersMock).not.toHaveBeenCalled(); expect(getUserByIdMock).not.toHaveBeenCalled(); expect(validateAuthTokenMock).toHaveBeenCalledWith("user-token", { allowReadOnlyAccess: true, @@ -211,7 +234,8 @@ describe("v1 users endpoints", () => { expect(Array.isArray(self.json.items[0].keys)).toBe(true); expect(JSON.stringify(self.json)).not.toContain("user-250"); expect(JSON.stringify(self.json)).not.toContain("fullKey"); - expect(getUsersMock).toHaveBeenCalledWith(); + expect(getCurrentUserDisplayMock).toHaveBeenCalledWith(); + expect(getUsersMock).not.toHaveBeenCalled(); expect(getUserByIdMock).not.toHaveBeenCalled(); }); diff --git a/tests/unit/api/v1/api-client-actions.test.ts b/tests/unit/api/v1/api-client-actions.test.ts index 7dff23d90..ef2b8eba4 100644 --- a/tests/unit/api/v1/api-client-actions.test.ts +++ b/tests/unit/api/v1/api-client-actions.test.ts @@ -147,9 +147,10 @@ describe("v1 action compatibility client", () => { pageInfo: { nextCursor: null, hasMore: false }, }); - await expect(users.getUsers()).resolves.toMatchObject([ - { id: 9, name: "self", keys: [{ id: 90, name: "default" }] }, - ]); + const result = await users.getUsers(); + + expect(result).toHaveLength(1); + expect(result).toMatchObject([{ id: 9, name: "self", keys: [{ id: 90, name: "default" }] }]); expect(getMock).toHaveBeenNthCalledWith(1, "/api/v1/users"); expect(getMock).toHaveBeenNthCalledWith(2, "/api/v1/users:self"); diff --git a/tests/unit/users-action-get-users-compat.test.ts b/tests/unit/users-action-get-users-compat.test.ts index d9fc19961..2dcf336e7 100644 --- a/tests/unit/users-action-get-users-compat.test.ts +++ b/tests/unit/users-action-get-users-compat.test.ts @@ -155,6 +155,64 @@ describe("getUsers compatibility", () => { expect(result[0]?.name).toBe("xiaolunanbei"); }); + test("getCurrentUserDisplay only loads the current session user", async () => { + getSessionMock.mockResolvedValueOnce({ + user: { id: 42, role: "admin" }, + key: { canLoginWebUi: true }, + }); + const currentUser = makeUser(42, "current-admin"); + currentUser.expiresAt = new Date("2026-05-07T07:41:10.000Z"); + findUserByIdMock.mockResolvedValueOnce(currentUser); + findKeyListBatchMock.mockResolvedValueOnce( + new Map([ + [ + 42, + [ + { + id: 420, + name: "default", + key: "sk-current-user-secret", + expiresAt: null, + isEnabled: true, + createdAt: new Date("2026-04-30T07:41:10.000Z"), + canLoginWebUi: true, + limit5hUsd: null, + limit5hResetMode: "rolling", + limitDailyUsd: null, + dailyResetMode: "fixed", + dailyResetTime: "00:00", + limitWeeklyUsd: null, + limitMonthlyUsd: null, + limitTotalUsd: null, + limitConcurrentSessions: 0, + costResetAt: null, + providerGroup: "default", + }, + ], + ], + ]) + ); + + const { getCurrentUserDisplay } = await import("@/actions/users"); + + const result = await getCurrentUserDisplay(); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.data).toMatchObject({ + id: 42, + name: "current-admin", + keys: [{ id: 420, name: "default", maskedKey: "sk-c••••••cret", canReveal: true }], + }); + expect(result.data.expiresAt).toBeInstanceOf(Date); + expect(result.data.keys[0]?.createdAt).toBeInstanceOf(Date); + expect(findUserByIdMock).toHaveBeenCalledWith(42); + expect(findUserListBatchMock).not.toHaveBeenCalled(); + expect(findKeyListBatchMock).toHaveBeenCalledWith([42]); + expect(findKeyUsageTodayBatchMock).toHaveBeenCalledWith([42]); + expect(findKeysStatisticsBatchFromKeysMock).toHaveBeenCalledWith(expect.any(Map)); + }); + test("getUsersBatchCore returns JSON-safe date fields for v1 API transport", async () => { const user = makeUser(88, "dated-user"); user.expiresAt = new Date("2026-05-07T07:41:10.000Z"); From d8d0f561daadf69819aa9ee597d41aa608fe15b9 Mon Sep 17 00:00:00 2001 From: Clouder0 Date: Thu, 21 May 2026 23:06:19 +0800 Subject: [PATCH 3/3] chore: remove unreachable self user guard --- src/actions/users.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/actions/users.ts b/src/actions/users.ts index 323f91131..1c543c155 100644 --- a/src/actions/users.ts +++ b/src/actions/users.ts @@ -521,14 +521,6 @@ export async function getCurrentUserDisplay(): Promise } const [displayUser] = await buildUserDisplays([user], session, session.user.role === "admin"); - if (!displayUser) { - return { - ok: false, - error: tError("USER_NOT_FOUND"), - errorCode: ERROR_CODES.NOT_FOUND, - }; - } - return { ok: true, data: displayUser }; } catch (error) { logger.error("Failed to fetch current user display data:", error);