Updating user API to user a single bulk endpoint rather than case sensitive named endpoints.
This commit is contained in:
parent
4d9de7f658
commit
f07ea5cc7e
|
@ -86,13 +86,16 @@ export const buildUserEndpoints = API => ({
|
|||
/**
|
||||
* Creates multiple users.
|
||||
* @param users the array of user objects to create
|
||||
* @param groups the array of group ids to add all users to
|
||||
*/
|
||||
createUsers: async ({ users, groups }) => {
|
||||
return await API.post({
|
||||
url: "/api/global/users/bulkCreate",
|
||||
url: "/api/global/users/bulk",
|
||||
body: {
|
||||
users,
|
||||
groups,
|
||||
create: {
|
||||
users,
|
||||
groups,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
|
@ -109,13 +112,15 @@ export const buildUserEndpoints = API => ({
|
|||
|
||||
/**
|
||||
* Deletes multiple users
|
||||
* @param userId the ID of the user to delete
|
||||
* @param userIds the ID of the user to delete
|
||||
*/
|
||||
deleteUsers: async userIds => {
|
||||
return await API.post({
|
||||
url: `/api/global/users/bulkDelete`,
|
||||
url: `/api/global/users/bulk`,
|
||||
body: {
|
||||
userIds,
|
||||
delete: {
|
||||
userIds,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
|
|
|
@ -6,28 +6,31 @@ export interface CreateUserResponse {
|
|||
email: string
|
||||
}
|
||||
|
||||
export interface BulkCreateUsersRequest {
|
||||
users: User[]
|
||||
groups: any[]
|
||||
}
|
||||
|
||||
export interface UserDetails {
|
||||
_id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface BulkCreateUsersResponse {
|
||||
successful: UserDetails[]
|
||||
unsuccessful: { email: string; reason: string }[]
|
||||
export interface BulkUserRequest {
|
||||
delete?: {
|
||||
userIds: string[]
|
||||
}
|
||||
create?: {
|
||||
users: User[]
|
||||
groups: any[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface BulkDeleteUsersRequest {
|
||||
userIds: string[]
|
||||
}
|
||||
|
||||
export interface BulkDeleteUsersResponse {
|
||||
successful: UserDetails[]
|
||||
unsuccessful: { _id: string; email: string; reason: string }[]
|
||||
export interface BulkUserResponse {
|
||||
created?: {
|
||||
successful: UserDetails[]
|
||||
unsuccessful: { email: string; reason: string }[]
|
||||
}
|
||||
deleted?: {
|
||||
successful: UserDetails[]
|
||||
unsuccessful: { _id: string; email: string; reason: string }[]
|
||||
}
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface InviteUserRequest {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { checkInviteCode } from "../../../utilities/redis"
|
||||
import { users } from "../../../sdk"
|
||||
import { users as userSdk } from "../../../sdk"
|
||||
import env from "../../../environment"
|
||||
import {
|
||||
BulkDeleteUsersRequest,
|
||||
BulkUserRequest,
|
||||
BulkUserResponse,
|
||||
CloudAccount,
|
||||
InviteUserRequest,
|
||||
InviteUsersRequest,
|
||||
|
@ -21,27 +22,43 @@ const MAX_USERS_UPLOAD_LIMIT = 1000
|
|||
|
||||
export const save = async (ctx: any) => {
|
||||
try {
|
||||
ctx.body = await users.save(ctx.request.body)
|
||||
ctx.body = await userSdk.save(ctx.request.body)
|
||||
} catch (err: any) {
|
||||
ctx.throw(err.status || 400, err)
|
||||
}
|
||||
}
|
||||
|
||||
export const bulkCreate = async (ctx: any) => {
|
||||
let { users: newUsersRequested, groups } = ctx.request.body
|
||||
const bulkDelete = async (userIds: string[], currentUserId: string) => {
|
||||
if (userIds?.indexOf(currentUserId) !== -1) {
|
||||
throw new Error("Unable to delete self.")
|
||||
}
|
||||
return await userSdk.bulkDelete(userIds)
|
||||
}
|
||||
|
||||
if (!env.SELF_HOSTED && newUsersRequested.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||
ctx.throw(
|
||||
400,
|
||||
const bulkCreate = async (users: User[], groupIds: string[]) => {
|
||||
if (!env.SELF_HOSTED && users.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||
throw new Error(
|
||||
"Max limit for upload is 1000 users. Please reduce file size and try again."
|
||||
)
|
||||
}
|
||||
return await userSdk.bulkCreate(users, groupIds)
|
||||
}
|
||||
|
||||
export const bulkUpdate = async (ctx: any) => {
|
||||
const currentUserId = ctx.user._id
|
||||
const input = ctx.request.body as BulkUserRequest
|
||||
let created, deleted
|
||||
try {
|
||||
ctx.body = await users.bulkCreate(newUsersRequested, groups)
|
||||
if (input.create) {
|
||||
created = await bulkCreate(input.create.users, input.create.groups)
|
||||
}
|
||||
if (input.delete) {
|
||||
deleted = await bulkDelete(input.delete.userIds, currentUserId)
|
||||
}
|
||||
} catch (err: any) {
|
||||
ctx.throw(err.status || 400, err)
|
||||
ctx.throw(400, err?.message || err)
|
||||
}
|
||||
ctx.body = { created, deleted } as BulkUserResponse
|
||||
}
|
||||
|
||||
const parseBooleanParam = (param: any) => {
|
||||
|
@ -85,7 +102,7 @@ export const adminUser = async (ctx: any) => {
|
|||
// always bust checklist beforehand, if an error occurs but can proceed, don't get
|
||||
// stuck in a cycle
|
||||
await cache.bustCache(cache.CacheKeys.CHECKLIST)
|
||||
const finalUser = await users.save(user, {
|
||||
const finalUser = await userSdk.save(user, {
|
||||
hashPassword,
|
||||
requirePassword,
|
||||
})
|
||||
|
@ -107,7 +124,7 @@ export const adminUser = async (ctx: any) => {
|
|||
export const countByApp = async (ctx: any) => {
|
||||
const appId = ctx.params.appId
|
||||
try {
|
||||
ctx.body = await users.countUsersByApp(appId)
|
||||
ctx.body = await userSdk.countUsersByApp(appId)
|
||||
} catch (err: any) {
|
||||
ctx.throw(err.status || 400, err)
|
||||
}
|
||||
|
@ -119,28 +136,15 @@ export const destroy = async (ctx: any) => {
|
|||
ctx.throw(400, "Unable to delete self.")
|
||||
}
|
||||
|
||||
await users.destroy(id, ctx.user)
|
||||
await userSdk.destroy(id, ctx.user)
|
||||
|
||||
ctx.body = {
|
||||
message: `User ${id} deleted.`,
|
||||
}
|
||||
}
|
||||
|
||||
export const bulkDelete = async (ctx: any) => {
|
||||
const { userIds } = ctx.request.body as BulkDeleteUsersRequest
|
||||
if (userIds?.indexOf(ctx.user._id) !== -1) {
|
||||
ctx.throw(400, "Unable to delete self.")
|
||||
}
|
||||
|
||||
try {
|
||||
ctx.body = await users.bulkDelete(userIds)
|
||||
} catch (err) {
|
||||
ctx.throw(err)
|
||||
}
|
||||
}
|
||||
|
||||
export const search = async (ctx: any) => {
|
||||
const paginated = await users.paginatedUsers(ctx.request.body)
|
||||
const paginated = await userSdk.paginatedUsers(ctx.request.body)
|
||||
// user hashed password shouldn't ever be returned
|
||||
for (let user of paginated.data) {
|
||||
if (user) {
|
||||
|
@ -152,7 +156,7 @@ export const search = async (ctx: any) => {
|
|||
|
||||
// called internally by app server user fetch
|
||||
export const fetch = async (ctx: any) => {
|
||||
const all = await users.allUsers()
|
||||
const all = await userSdk.allUsers()
|
||||
// user hashed password shouldn't ever be returned
|
||||
for (let user of all) {
|
||||
if (user) {
|
||||
|
@ -164,7 +168,7 @@ export const fetch = async (ctx: any) => {
|
|||
|
||||
// called internally by app server user find
|
||||
export const find = async (ctx: any) => {
|
||||
ctx.body = await users.getUser(ctx.params.id)
|
||||
ctx.body = await userSdk.getUser(ctx.params.id)
|
||||
}
|
||||
|
||||
export const tenantUserLookup = async (ctx: any) => {
|
||||
|
@ -179,7 +183,7 @@ export const tenantUserLookup = async (ctx: any) => {
|
|||
|
||||
export const invite = async (ctx: any) => {
|
||||
const request = ctx.request.body as InviteUserRequest
|
||||
const response = await users.invite([request])
|
||||
const response = await userSdk.invite([request])
|
||||
|
||||
// explicitly throw for single user invite
|
||||
if (response.unsuccessful.length) {
|
||||
|
@ -198,7 +202,7 @@ export const invite = async (ctx: any) => {
|
|||
|
||||
export const inviteMultiple = async (ctx: any) => {
|
||||
const request = ctx.request.body as InviteUsersRequest
|
||||
ctx.body = await users.invite(request)
|
||||
ctx.body = await userSdk.invite(request)
|
||||
}
|
||||
|
||||
export const inviteAccept = async (ctx: any) => {
|
||||
|
@ -207,7 +211,7 @@ export const inviteAccept = async (ctx: any) => {
|
|||
// info is an extension of the user object that was stored by global
|
||||
const { email, info }: any = await checkInviteCode(inviteCode)
|
||||
ctx.body = await tenancy.doInTenant(info.tenantId, async () => {
|
||||
const saved = await users.save({
|
||||
const saved = await userSdk.save({
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
|
|
|
@ -97,16 +97,16 @@ describe("/api/global/users", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("bulkCreate", () => {
|
||||
describe("bulk (create)", () => {
|
||||
it("should ignore users existing in the same tenant", async () => {
|
||||
const user = await config.createUser()
|
||||
jest.clearAllMocks()
|
||||
|
||||
const response = await api.users.bulkCreateUsers([user])
|
||||
|
||||
expect(response.successful.length).toBe(0)
|
||||
expect(response.unsuccessful.length).toBe(1)
|
||||
expect(response.unsuccessful[0].email).toBe(user.email)
|
||||
expect(response.created?.successful.length).toBe(0)
|
||||
expect(response.created?.unsuccessful.length).toBe(1)
|
||||
expect(response.created?.unsuccessful[0].email).toBe(user.email)
|
||||
expect(events.user.created).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
|
@ -117,9 +117,9 @@ describe("/api/global/users", () => {
|
|||
await tenancy.doInTenant(TENANT_1, async () => {
|
||||
const response = await api.users.bulkCreateUsers([user])
|
||||
|
||||
expect(response.successful.length).toBe(0)
|
||||
expect(response.unsuccessful.length).toBe(1)
|
||||
expect(response.unsuccessful[0].email).toBe(user.email)
|
||||
expect(response.created?.successful.length).toBe(0)
|
||||
expect(response.created?.unsuccessful.length).toBe(1)
|
||||
expect(response.created?.unsuccessful[0].email).toBe(user.email)
|
||||
expect(events.user.created).toBeCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
@ -132,24 +132,24 @@ describe("/api/global/users", () => {
|
|||
|
||||
const response = await api.users.bulkCreateUsers([user])
|
||||
|
||||
expect(response.successful.length).toBe(0)
|
||||
expect(response.unsuccessful.length).toBe(1)
|
||||
expect(response.unsuccessful[0].email).toBe(user.email)
|
||||
expect(response.created?.successful.length).toBe(0)
|
||||
expect(response.created?.unsuccessful.length).toBe(1)
|
||||
expect(response.created?.unsuccessful[0].email).toBe(user.email)
|
||||
expect(events.user.created).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
it("should be able to bulkCreate users", async () => {
|
||||
it("should be able to bulk create users", async () => {
|
||||
const builder = structures.users.builderUser()
|
||||
const admin = structures.users.adminUser()
|
||||
const user = structures.users.user()
|
||||
|
||||
const response = await api.users.bulkCreateUsers([builder, admin, user])
|
||||
|
||||
expect(response.successful.length).toBe(3)
|
||||
expect(response.successful[0].email).toBe(builder.email)
|
||||
expect(response.successful[1].email).toBe(admin.email)
|
||||
expect(response.successful[2].email).toBe(user.email)
|
||||
expect(response.unsuccessful.length).toBe(0)
|
||||
expect(response.created?.successful.length).toBe(3)
|
||||
expect(response.created?.successful[0].email).toBe(builder.email)
|
||||
expect(response.created?.successful[1].email).toBe(admin.email)
|
||||
expect(response.created?.successful[2].email).toBe(user.email)
|
||||
expect(response.created?.unsuccessful.length).toBe(0)
|
||||
expect(events.user.created).toBeCalledTimes(3)
|
||||
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
|
||||
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(2)
|
||||
|
@ -420,33 +420,30 @@ describe("/api/global/users", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("bulkDelete", () => {
|
||||
it("should not be able to bulkDelete current user", async () => {
|
||||
describe("bulk (delete)", () => {
|
||||
it("should not be able to bulk delete current user", async () => {
|
||||
const user = await config.defaultUser!
|
||||
const request = { userIds: [user._id!] }
|
||||
|
||||
const response = await api.users.bulkDeleteUsers(request, 400)
|
||||
const response = await api.users.bulkDeleteUsers([user._id!], 400)
|
||||
|
||||
expect(response.body.message).toBe("Unable to delete self.")
|
||||
expect(response.message).toBe("Unable to delete self.")
|
||||
expect(events.user.deleted).not.toBeCalled()
|
||||
})
|
||||
|
||||
it("should not be able to bulkDelete account owner", async () => {
|
||||
it("should not be able to bulk delete account owner", async () => {
|
||||
const user = await config.createUser()
|
||||
const account = structures.accounts.cloudAccount()
|
||||
account.budibaseUserId = user._id!
|
||||
mocks.accounts.getAccountByTenantId.mockReturnValue(account)
|
||||
|
||||
const request = { userIds: [user._id!] }
|
||||
const response = await api.users.bulkDeleteUsers([user._id!])
|
||||
|
||||
const response = await api.users.bulkDeleteUsers(request)
|
||||
|
||||
expect(response.body.successful.length).toBe(0)
|
||||
expect(response.body.unsuccessful.length).toBe(1)
|
||||
expect(response.body.unsuccessful[0].reason).toBe(
|
||||
expect(response.deleted?.successful.length).toBe(0)
|
||||
expect(response.deleted?.unsuccessful.length).toBe(1)
|
||||
expect(response.deleted?.unsuccessful[0].reason).toBe(
|
||||
"Account holder cannot be deleted"
|
||||
)
|
||||
expect(response.body.unsuccessful[0]._id).toBe(user._id)
|
||||
expect(response.deleted?.unsuccessful[0]._id).toBe(user._id)
|
||||
expect(events.user.deleted).not.toBeCalled()
|
||||
})
|
||||
|
||||
|
@ -462,12 +459,14 @@ describe("/api/global/users", () => {
|
|||
admin,
|
||||
user,
|
||||
])
|
||||
const request = { userIds: createdUsers.successful.map(u => u._id!) }
|
||||
|
||||
const response = await api.users.bulkDeleteUsers(request)
|
||||
const toDelete = createdUsers.created?.successful.map(
|
||||
u => u._id!
|
||||
) as string[]
|
||||
const response = await api.users.bulkDeleteUsers(toDelete)
|
||||
|
||||
expect(response.body.successful.length).toBe(3)
|
||||
expect(response.body.unsuccessful.length).toBe(0)
|
||||
expect(response.deleted?.successful.length).toBe(3)
|
||||
expect(response.deleted?.unsuccessful.length).toBe(0)
|
||||
expect(events.user.deleted).toBeCalledTimes(3)
|
||||
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
|
||||
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(2)
|
||||
|
|
|
@ -56,16 +56,15 @@ router
|
|||
controller.save
|
||||
)
|
||||
.post(
|
||||
"/api/global/users/bulkCreate",
|
||||
"/api/global/users/bulk",
|
||||
adminOnly,
|
||||
users.buildUserBulkSaveValidation(),
|
||||
controller.bulkCreate
|
||||
users.buildUserBulkUserValidation(),
|
||||
controller.bulkUpdate
|
||||
)
|
||||
|
||||
.get("/api/global/users", builderOrAdmin, controller.fetch)
|
||||
.post("/api/global/users/search", builderOrAdmin, controller.search)
|
||||
.delete("/api/global/users/:id", adminOnly, controller.destroy)
|
||||
.post("/api/global/users/bulkDelete", adminOnly, controller.bulkDelete)
|
||||
.get("/api/global/users/count/:appId", builderOrAdmin, controller.countByApp)
|
||||
.get("/api/global/roles/:appId")
|
||||
.post(
|
||||
|
|
|
@ -28,7 +28,7 @@ export const buildUserSaveValidation = (isSelf = false) => {
|
|||
return joiValidator.body(Joi.object(schema).required().unknown(true))
|
||||
}
|
||||
|
||||
export const buildUserBulkSaveValidation = (isSelf = false) => {
|
||||
export const buildUserBulkUserValidation = (isSelf = false) => {
|
||||
if (!isSelf) {
|
||||
schema = {
|
||||
...schema,
|
||||
|
@ -36,10 +36,15 @@ export const buildUserBulkSaveValidation = (isSelf = false) => {
|
|||
_rev: Joi.string(),
|
||||
}
|
||||
}
|
||||
let bulkSaveSchema = {
|
||||
groups: Joi.array().optional(),
|
||||
users: Joi.array().items(Joi.object(schema).required().unknown(true)),
|
||||
let bulkSchema = {
|
||||
create: Joi.object({
|
||||
groups: Joi.array().optional(),
|
||||
users: Joi.array().items(Joi.object(schema).required().unknown(true)),
|
||||
}),
|
||||
delete: Joi.object({
|
||||
userIds: Joi.array().items(Joi.string()),
|
||||
}),
|
||||
}
|
||||
|
||||
return joiValidator.body(Joi.object(bulkSaveSchema).required().unknown(true))
|
||||
return joiValidator.body(Joi.object(bulkSchema).required().unknown(true))
|
||||
}
|
||||
|
|
|
@ -19,8 +19,7 @@ import {
|
|||
import {
|
||||
AccountMetadata,
|
||||
AllDocsResponse,
|
||||
BulkCreateUsersResponse,
|
||||
BulkDeleteUsersResponse,
|
||||
BulkUserResponse,
|
||||
CloudAccount,
|
||||
CreateUserResponse,
|
||||
InviteUsersRequest,
|
||||
|
@ -347,7 +346,7 @@ const searchExistingEmails = async (emails: string[]) => {
|
|||
export const bulkCreate = async (
|
||||
newUsersRequested: User[],
|
||||
groups: string[]
|
||||
): Promise<BulkCreateUsersResponse> => {
|
||||
): Promise<BulkUserResponse["created"]> => {
|
||||
const db = tenancy.getGlobalDB()
|
||||
const tenantId = tenancy.getTenantId()
|
||||
|
||||
|
@ -436,10 +435,10 @@ const getAccountHolderFromUserIds = async (
|
|||
|
||||
export const bulkDelete = async (
|
||||
userIds: string[]
|
||||
): Promise<BulkDeleteUsersResponse> => {
|
||||
): Promise<BulkUserResponse["deleted"]> => {
|
||||
const db = tenancy.getGlobalDB()
|
||||
|
||||
const response: BulkDeleteUsersResponse = {
|
||||
const response: BulkUserResponse["deleted"] = {
|
||||
successful: [],
|
||||
unsuccessful: [],
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {
|
||||
BulkCreateUsersRequest,
|
||||
BulkCreateUsersResponse,
|
||||
BulkDeleteUsersRequest,
|
||||
BulkDeleteUsersResponse,
|
||||
BulkUserResponse,
|
||||
BulkUserRequest,
|
||||
InviteUsersRequest,
|
||||
User,
|
||||
} from "@budibase/types"
|
||||
|
@ -69,24 +67,26 @@ export class UserAPI {
|
|||
// BULK
|
||||
|
||||
bulkCreateUsers = async (users: User[], groups: any[] = []) => {
|
||||
const body: BulkCreateUsersRequest = { users, groups }
|
||||
const body: BulkUserRequest = { create: { users, groups } }
|
||||
const res = await this.request
|
||||
.post(`/api/global/users/bulkCreate`)
|
||||
.post(`/api/global/users/bulk`)
|
||||
.send(body)
|
||||
.set(this.config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
return res.body as BulkCreateUsersResponse
|
||||
return res.body as BulkUserResponse
|
||||
}
|
||||
|
||||
bulkDeleteUsers = (body: BulkDeleteUsersRequest, status?: number) => {
|
||||
return this.request
|
||||
.post(`/api/global/users/bulkDelete`)
|
||||
bulkDeleteUsers = async (userIds: string[], status?: number) => {
|
||||
const body: BulkUserRequest = { delete: { userIds } }
|
||||
const res = await this.request
|
||||
.post(`/api/global/users/bulk`)
|
||||
.send(body)
|
||||
.set(this.config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(status ? status : 200)
|
||||
return res.body as BulkUserResponse
|
||||
}
|
||||
|
||||
// USER
|
||||
|
|
Loading…
Reference in New Issue