Updating user API to user a single bulk endpoint rather than case sensitive named endpoints.

This commit is contained in:
mike12345567 2022-09-21 18:05:45 +01:00
parent 4d9de7f658
commit f07ea5cc7e
8 changed files with 124 additions and 110 deletions

View File

@ -86,14 +86,17 @@ 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: {
create: {
users,
groups,
},
},
})
},
@ -109,14 +112,16 @@ 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: {
delete: {
userIds,
},
},
})
},

View File

@ -6,29 +6,32 @@ export interface CreateUserResponse {
email: string
}
export interface BulkCreateUsersRequest {
users: User[]
groups: any[]
}
export interface UserDetails {
_id: string
email: string
}
export interface BulkCreateUsersResponse {
export interface BulkUserRequest {
delete?: {
userIds: string[]
}
create?: {
users: User[]
groups: any[]
}
}
export interface BulkUserResponse {
created?: {
successful: UserDetails[]
unsuccessful: { email: string; reason: string }[]
}
export interface BulkDeleteUsersRequest {
userIds: string[]
}
export interface BulkDeleteUsersResponse {
deleted?: {
successful: UserDetails[]
unsuccessful: { _id: string; email: string; reason: string }[]
}
message?: string
}
export interface InviteUserRequest {
email: string

View File

@ -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."
)
}
try {
ctx.body = await users.bulkCreate(newUsersRequested, groups)
} catch (err: any) {
ctx.throw(err.status || 400, err)
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 {
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(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,

View File

@ -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)

View File

@ -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(

View File

@ -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 = {
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))
}

View File

@ -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: [],
}

View File

@ -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