diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 58979ec799..b0e3219656 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -26,6 +26,7 @@ import { migrations, platform, tenancy, + db, } from "@budibase/backend-core" import { checkAnyUserExists } from "../../../utilities/users" import { isEmailConfigured } from "../../../utilities/email" @@ -185,9 +186,27 @@ export const getAppUsers = async (ctx: Ctx) => { export const search = async (ctx: Ctx) => { const body = ctx.request.body - // TODO: for now only one supported search key, string.email - if (body?.query && !userSdk.core.isSupportedUserSearch(body.query)) { - ctx.throw(501, "Can only search by string.email or equal._id") + // TODO: for now only two supported search keys; string.email and equal._id + if (body?.query) { + // Clean numeric prefixing. This will overwrite duplicate search fields, + // but this is fine because we only support a single custom search on + // email and id + for (let filters of Object.values(body.query)) { + if (filters && typeof filters === "object") { + for (let [field, value] of Object.entries(filters)) { + delete filters[field] + const cleanedField = db.removeKeyNumbering(field) + if (filters[cleanedField] !== undefined) { + ctx.throw(400, "Only 1 filter per field is supported") + } + filters[cleanedField] = value + } + } + } + // Validate we aren't trying to search on any illegal fields + if (!userSdk.core.isSupportedUserSearch(body.query)) { + ctx.throw(400, "Can only search by string.email or equal._id") + } } if (body.paginate === false) { diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index a85933255a..c792de70a9 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -590,6 +590,15 @@ describe("/api/global/users", () => { expect(response.body.data[0].email).toBe(user.email) }) + it("should be able to search by email with numeric prefixing", async () => { + const user = await config.createUser() + const response = await config.api.users.searchUsers({ + query: { string: { ["999:email"]: user.email } }, + }) + expect(response.body.data.length).toBe(1) + expect(response.body.data[0].email).toBe(user.email) + }) + it("should be able to search by _id", async () => { const user = await config.createUser() const response = await config.api.users.searchUsers({ @@ -599,13 +608,52 @@ describe("/api/global/users", () => { expect(response.body.data[0]._id).toBe(user._id) }) + it("should be able to search by _id with numeric prefixing", async () => { + const user = await config.createUser() + const response = await config.api.users.searchUsers({ + query: { equal: { ["1:_id"]: user._id } }, + }) + expect(response.body.data.length).toBe(1) + expect(response.body.data[0]._id).toBe(user._id) + }) + + it("should throw an error when using multiple filters on the same field", async () => { + const user = await config.createUser() + await config.api.users.searchUsers( + { + query: { + string: { + ["1:email"]: user.email, + ["2:email"]: "something else", + }, + }, + }, + { status: 400 } + ) + }) + + it("should throw an error when using multiple filters on the same field without prefixes", async () => { + const user = await config.createUser() + await config.api.users.searchUsers( + { + query: { + string: { + ["_id"]: user.email, + ["999:_id"]: "something else", + }, + }, + }, + { status: 400 } + ) + }) + it("should throw an error when unimplemented options used", async () => { const user = await config.createUser() await config.api.users.searchUsers( { query: { equal: { firstName: user.firstName } }, }, - { status: 501 } + { status: 400 } ) })