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 120ea88aff
commit 39689d27f6
8 changed files with 124 additions and 110 deletions

View File

@ -86,14 +86,17 @@ export const buildUserEndpoints = API => ({
/** /**
* Creates multiple users. * Creates multiple users.
* @param users the array of user objects to create * @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 }) => { createUsers: async ({ users, groups }) => {
return await API.post({ return await API.post({
url: "/api/global/users/bulkCreate", url: "/api/global/users/bulk",
body: { body: {
create: {
users, users,
groups, groups,
}, },
},
}) })
}, },
@ -109,14 +112,16 @@ export const buildUserEndpoints = API => ({
/** /**
* Deletes multiple users * Deletes multiple users
* @param userId the ID of the user to delete * @param userIds the ID of the user to delete
*/ */
deleteUsers: async userIds => { deleteUsers: async userIds => {
return await API.post({ return await API.post({
url: `/api/global/users/bulkDelete`, url: `/api/global/users/bulk`,
body: { body: {
delete: {
userIds, userIds,
}, },
},
}) })
}, },

View File

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

View File

@ -1,8 +1,9 @@
import { checkInviteCode } from "../../../utilities/redis" import { checkInviteCode } from "../../../utilities/redis"
import { users } from "../../../sdk" import { users as userSdk } from "../../../sdk"
import env from "../../../environment" import env from "../../../environment"
import { import {
BulkDeleteUsersRequest, BulkUserRequest,
BulkUserResponse,
CloudAccount, CloudAccount,
InviteUserRequest, InviteUserRequest,
InviteUsersRequest, InviteUsersRequest,
@ -21,27 +22,43 @@ const MAX_USERS_UPLOAD_LIMIT = 1000
export const save = async (ctx: any) => { export const save = async (ctx: any) => {
try { try {
ctx.body = await users.save(ctx.request.body) ctx.body = await userSdk.save(ctx.request.body)
} catch (err: any) { } catch (err: any) {
ctx.throw(err.status || 400, err) ctx.throw(err.status || 400, err)
} }
} }
export const bulkCreate = async (ctx: any) => { const bulkDelete = async (userIds: string[], currentUserId: string) => {
let { users: newUsersRequested, groups } = ctx.request.body 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) { const bulkCreate = async (users: User[], groupIds: string[]) => {
ctx.throw( if (!env.SELF_HOSTED && users.length > MAX_USERS_UPLOAD_LIMIT) {
400, throw new Error(
"Max limit for upload is 1000 users. Please reduce file size and try again." "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 { try {
ctx.body = await users.bulkCreate(newUsersRequested, groups) if (input.create) {
} catch (err: any) { created = await bulkCreate(input.create.users, input.create.groups)
ctx.throw(err.status || 400, err)
} }
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) => { 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 // always bust checklist beforehand, if an error occurs but can proceed, don't get
// stuck in a cycle // stuck in a cycle
await cache.bustCache(cache.CacheKeys.CHECKLIST) await cache.bustCache(cache.CacheKeys.CHECKLIST)
const finalUser = await users.save(user, { const finalUser = await userSdk.save(user, {
hashPassword, hashPassword,
requirePassword, requirePassword,
}) })
@ -107,7 +124,7 @@ export const adminUser = async (ctx: any) => {
export const countByApp = async (ctx: any) => { export const countByApp = async (ctx: any) => {
const appId = ctx.params.appId const appId = ctx.params.appId
try { try {
ctx.body = await users.countUsersByApp(appId) ctx.body = await userSdk.countUsersByApp(appId)
} catch (err: any) { } catch (err: any) {
ctx.throw(err.status || 400, err) ctx.throw(err.status || 400, err)
} }
@ -119,28 +136,15 @@ export const destroy = async (ctx: any) => {
ctx.throw(400, "Unable to delete self.") ctx.throw(400, "Unable to delete self.")
} }
await users.destroy(id, ctx.user) await userSdk.destroy(id, ctx.user)
ctx.body = { ctx.body = {
message: `User ${id} deleted.`, 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) => { 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 // user hashed password shouldn't ever be returned
for (let user of paginated.data) { for (let user of paginated.data) {
if (user) { if (user) {
@ -152,7 +156,7 @@ export const search = async (ctx: any) => {
// called internally by app server user fetch // called internally by app server user fetch
export const fetch = async (ctx: any) => { export const fetch = async (ctx: any) => {
const all = await users.allUsers() const all = await userSdk.allUsers()
// user hashed password shouldn't ever be returned // user hashed password shouldn't ever be returned
for (let user of all) { for (let user of all) {
if (user) { if (user) {
@ -164,7 +168,7 @@ export const fetch = async (ctx: any) => {
// called internally by app server user find // called internally by app server user find
export const find = async (ctx: any) => { 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) => { export const tenantUserLookup = async (ctx: any) => {
@ -179,7 +183,7 @@ export const tenantUserLookup = async (ctx: any) => {
export const invite = async (ctx: any) => { export const invite = async (ctx: any) => {
const request = ctx.request.body as InviteUserRequest 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 // explicitly throw for single user invite
if (response.unsuccessful.length) { if (response.unsuccessful.length) {
@ -198,7 +202,7 @@ export const invite = async (ctx: any) => {
export const inviteMultiple = async (ctx: any) => { export const inviteMultiple = async (ctx: any) => {
const request = ctx.request.body as InviteUsersRequest 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) => { 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 // info is an extension of the user object that was stored by global
const { email, info }: any = await checkInviteCode(inviteCode) const { email, info }: any = await checkInviteCode(inviteCode)
ctx.body = await tenancy.doInTenant(info.tenantId, async () => { ctx.body = await tenancy.doInTenant(info.tenantId, async () => {
const saved = await users.save({ const saved = await userSdk.save({
firstName, firstName,
lastName, lastName,
password, 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 () => { it("should ignore users existing in the same tenant", async () => {
const user = await config.createUser() const user = await config.createUser()
jest.clearAllMocks() jest.clearAllMocks()
const response = await api.users.bulkCreateUsers([user]) const response = await api.users.bulkCreateUsers([user])
expect(response.successful.length).toBe(0) expect(response.created?.successful.length).toBe(0)
expect(response.unsuccessful.length).toBe(1) expect(response.created?.unsuccessful.length).toBe(1)
expect(response.unsuccessful[0].email).toBe(user.email) expect(response.created?.unsuccessful[0].email).toBe(user.email)
expect(events.user.created).toBeCalledTimes(0) expect(events.user.created).toBeCalledTimes(0)
}) })
@ -117,9 +117,9 @@ describe("/api/global/users", () => {
await tenancy.doInTenant(TENANT_1, async () => { await tenancy.doInTenant(TENANT_1, async () => {
const response = await api.users.bulkCreateUsers([user]) const response = await api.users.bulkCreateUsers([user])
expect(response.successful.length).toBe(0) expect(response.created?.successful.length).toBe(0)
expect(response.unsuccessful.length).toBe(1) expect(response.created?.unsuccessful.length).toBe(1)
expect(response.unsuccessful[0].email).toBe(user.email) expect(response.created?.unsuccessful[0].email).toBe(user.email)
expect(events.user.created).toBeCalledTimes(0) expect(events.user.created).toBeCalledTimes(0)
}) })
}) })
@ -132,24 +132,24 @@ describe("/api/global/users", () => {
const response = await api.users.bulkCreateUsers([user]) const response = await api.users.bulkCreateUsers([user])
expect(response.successful.length).toBe(0) expect(response.created?.successful.length).toBe(0)
expect(response.unsuccessful.length).toBe(1) expect(response.created?.unsuccessful.length).toBe(1)
expect(response.unsuccessful[0].email).toBe(user.email) expect(response.created?.unsuccessful[0].email).toBe(user.email)
expect(events.user.created).toBeCalledTimes(0) 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 builder = structures.users.builderUser()
const admin = structures.users.adminUser() const admin = structures.users.adminUser()
const user = structures.users.user() const user = structures.users.user()
const response = await api.users.bulkCreateUsers([builder, admin, user]) const response = await api.users.bulkCreateUsers([builder, admin, user])
expect(response.successful.length).toBe(3) expect(response.created?.successful.length).toBe(3)
expect(response.successful[0].email).toBe(builder.email) expect(response.created?.successful[0].email).toBe(builder.email)
expect(response.successful[1].email).toBe(admin.email) expect(response.created?.successful[1].email).toBe(admin.email)
expect(response.successful[2].email).toBe(user.email) expect(response.created?.successful[2].email).toBe(user.email)
expect(response.unsuccessful.length).toBe(0) expect(response.created?.unsuccessful.length).toBe(0)
expect(events.user.created).toBeCalledTimes(3) expect(events.user.created).toBeCalledTimes(3)
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(2) expect(events.user.permissionBuilderAssigned).toBeCalledTimes(2)
@ -420,33 +420,30 @@ describe("/api/global/users", () => {
}) })
}) })
describe("bulkDelete", () => { describe("bulk (delete)", () => {
it("should not be able to bulkDelete current user", async () => { it("should not be able to bulk delete current user", async () => {
const user = await config.defaultUser! 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() 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 user = await config.createUser()
const account = structures.accounts.cloudAccount() const account = structures.accounts.cloudAccount()
account.budibaseUserId = user._id! account.budibaseUserId = user._id!
mocks.accounts.getAccountByTenantId.mockReturnValue(account) 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.deleted?.successful.length).toBe(0)
expect(response.deleted?.unsuccessful.length).toBe(1)
expect(response.body.successful.length).toBe(0) expect(response.deleted?.unsuccessful[0].reason).toBe(
expect(response.body.unsuccessful.length).toBe(1)
expect(response.body.unsuccessful[0].reason).toBe(
"Account holder cannot be deleted" "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() expect(events.user.deleted).not.toBeCalled()
}) })
@ -462,12 +459,14 @@ describe("/api/global/users", () => {
admin, admin,
user, 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.deleted?.successful.length).toBe(3)
expect(response.body.unsuccessful.length).toBe(0) expect(response.deleted?.unsuccessful.length).toBe(0)
expect(events.user.deleted).toBeCalledTimes(3) expect(events.user.deleted).toBeCalledTimes(3)
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(2) expect(events.user.permissionBuilderRemoved).toBeCalledTimes(2)

View File

@ -56,16 +56,15 @@ router
controller.save controller.save
) )
.post( .post(
"/api/global/users/bulkCreate", "/api/global/users/bulk",
adminOnly, adminOnly,
users.buildUserBulkSaveValidation(), users.buildUserBulkUserValidation(),
controller.bulkCreate controller.bulkUpdate
) )
.get("/api/global/users", builderOrAdmin, controller.fetch) .get("/api/global/users", builderOrAdmin, controller.fetch)
.post("/api/global/users/search", builderOrAdmin, controller.search) .post("/api/global/users/search", builderOrAdmin, controller.search)
.delete("/api/global/users/:id", adminOnly, controller.destroy) .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/users/count/:appId", builderOrAdmin, controller.countByApp)
.get("/api/global/roles/:appId") .get("/api/global/roles/:appId")
.post( .post(

View File

@ -28,7 +28,7 @@ export const buildUserSaveValidation = (isSelf = false) => {
return joiValidator.body(Joi.object(schema).required().unknown(true)) return joiValidator.body(Joi.object(schema).required().unknown(true))
} }
export const buildUserBulkSaveValidation = (isSelf = false) => { export const buildUserBulkUserValidation = (isSelf = false) => {
if (!isSelf) { if (!isSelf) {
schema = { schema = {
...schema, ...schema,
@ -36,10 +36,15 @@ export const buildUserBulkSaveValidation = (isSelf = false) => {
_rev: Joi.string(), _rev: Joi.string(),
} }
} }
let bulkSaveSchema = { let bulkSchema = {
create: Joi.object({
groups: Joi.array().optional(), groups: Joi.array().optional(),
users: Joi.array().items(Joi.object(schema).required().unknown(true)), 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 { import {
AccountMetadata, AccountMetadata,
AllDocsResponse, AllDocsResponse,
BulkCreateUsersResponse, BulkUserResponse,
BulkDeleteUsersResponse,
CloudAccount, CloudAccount,
CreateUserResponse, CreateUserResponse,
InviteUsersRequest, InviteUsersRequest,
@ -347,7 +346,7 @@ const searchExistingEmails = async (emails: string[]) => {
export const bulkCreate = async ( export const bulkCreate = async (
newUsersRequested: User[], newUsersRequested: User[],
groups: string[] groups: string[]
): Promise<BulkCreateUsersResponse> => { ): Promise<BulkUserResponse["created"]> => {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
const tenantId = tenancy.getTenantId() const tenantId = tenancy.getTenantId()
@ -436,10 +435,10 @@ const getAccountHolderFromUserIds = async (
export const bulkDelete = async ( export const bulkDelete = async (
userIds: string[] userIds: string[]
): Promise<BulkDeleteUsersResponse> => { ): Promise<BulkUserResponse["deleted"]> => {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
const response: BulkDeleteUsersResponse = { const response: BulkUserResponse["deleted"] = {
successful: [], successful: [],
unsuccessful: [], unsuccessful: [],
} }

View File

@ -1,8 +1,6 @@
import { import {
BulkCreateUsersRequest, BulkUserResponse,
BulkCreateUsersResponse, BulkUserRequest,
BulkDeleteUsersRequest,
BulkDeleteUsersResponse,
InviteUsersRequest, InviteUsersRequest,
User, User,
} from "@budibase/types" } from "@budibase/types"
@ -69,24 +67,26 @@ export class UserAPI {
// BULK // BULK
bulkCreateUsers = async (users: User[], groups: any[] = []) => { bulkCreateUsers = async (users: User[], groups: any[] = []) => {
const body: BulkCreateUsersRequest = { users, groups } const body: BulkUserRequest = { create: { users, groups } }
const res = await this.request const res = await this.request
.post(`/api/global/users/bulkCreate`) .post(`/api/global/users/bulk`)
.send(body) .send(body)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
return res.body as BulkCreateUsersResponse return res.body as BulkUserResponse
} }
bulkDeleteUsers = (body: BulkDeleteUsersRequest, status?: number) => { bulkDeleteUsers = async (userIds: string[], status?: number) => {
return this.request const body: BulkUserRequest = { delete: { userIds } }
.post(`/api/global/users/bulkDelete`) const res = await this.request
.post(`/api/global/users/bulk`)
.send(body) .send(body)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(status ? status : 200) .expect(status ? status : 200)
return res.body as BulkUserResponse
} }
// USER // USER