diff --git a/.changeset/cool-cups-dig.md b/.changeset/cool-cups-dig.md new file mode 100644 index 0000000000..4b4b7693c9 --- /dev/null +++ b/.changeset/cool-cups-dig.md @@ -0,0 +1,5 @@ +--- +"better-auth": patch +--- + +The admin plugin's `unbanUser`, `setRole` and `adminUpdateUser` endpoints used to call `internalAdapter.updateUser` without checking that the target user existed, so when the caller passed an unknown id the underlying database error (for example Prisma's `P2025`) bubbled up as a generic HTTP 500. those endpoints now mirror the existing guard in `banUser`: look the user up via `findUserById`, and throw a clean `NOT_FOUND` (`USER_NOT_FOUND`) when no row is returned. Closes #9800. diff --git a/packages/better-auth/src/plugins/admin/admin.test.ts b/packages/better-auth/src/plugins/admin/admin.test.ts index 16a0ec75f2..5bc8fd969d 100644 --- a/packages/better-auth/src/plugins/admin/admin.test.ts +++ b/packages/better-auth/src/plugins/admin/admin.test.ts @@ -719,6 +719,62 @@ describe("Admin plugin", async () => { expect(res.data?.sessions.length).toBe(1); }); + it("should return 404 from unbanUser when the target user does not exist", async () => { + const res = await client.admin.unbanUser( + { + userId: "nonexistent-user-id", + }, + { + headers: adminHeaders, + }, + ); + expect(res.error?.status).toBe(404); + expect(res.error?.code).toBe("USER_NOT_FOUND"); + }); + + it("should return 404 from banUser when the target user does not exist", async () => { + const res = await client.admin.banUser( + { + userId: "nonexistent-user-id", + }, + { + headers: adminHeaders, + }, + ); + expect(res.error?.status).toBe(404); + expect(res.error?.code).toBe("USER_NOT_FOUND"); + }); + + it("should return 404 from setRole when the target user does not exist", async () => { + const res = await client.admin.setRole( + { + userId: "nonexistent-user-id", + role: "admin", + }, + { + headers: adminHeaders, + }, + ); + expect(res.error?.status).toBe(404); + expect(res.error?.code).toBe("USER_NOT_FOUND"); + }); + + it("should return 404 from admin.updateUser when the target user does not exist", async () => { + const res = await client.admin.updateUser( + { + userId: "nonexistent-user-id", + data: { + name: "John Doe", + }, + }, + { + headers: adminHeaders, + }, + ); + expect(res.error?.status).toBe(404); + expect(res.error?.code).toBe("USER_NOT_FOUND"); + }); + it("should not allow non-admin to list user sessions", async () => { const res = await client.admin.listUserSessions( { diff --git a/packages/better-auth/src/plugins/admin/routes.ts b/packages/better-auth/src/plugins/admin/routes.ts index f79986fe27..e26b13b87d 100644 --- a/packages/better-auth/src/plugins/admin/routes.ts +++ b/packages/better-auth/src/plugins/admin/routes.ts @@ -153,6 +153,14 @@ export const setRole = (opts: O) => } } } + + const isUserExist = await ctx.context.internalAdapter.findUserById( + ctx.body.userId, + ); + if (!isUserExist) { + throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND); + } + const updatedUser = await ctx.context.internalAdapter.updateUser( ctx.body.userId, { @@ -507,6 +515,14 @@ export const adminUpdateUser = (opts: AdminOptions) => inputRoles as string[], ); } + + const isUserExist = await ctx.context.internalAdapter.findUserById( + ctx.body.userId, + ); + if (!isUserExist) { + throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND); + } + const updatedUser = await ctx.context.internalAdapter.updateUser( ctx.body.userId, ctx.body.data, @@ -851,6 +867,13 @@ export const unbanUser = (opts: AdminOptions) => ); } + const isUserExist = await ctx.context.internalAdapter.findUserById( + ctx.body.userId, + ); + if (!isUserExist) { + throw APIError.from("NOT_FOUND", BASE_ERROR_CODES.USER_NOT_FOUND); + } + const user = await ctx.context.internalAdapter.updateUser( ctx.body.userId, {