@budibase/worker 1.2.57 → 1.2.58-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/scripts/jestSetup.js +6 -0
- package/src/api/controllers/global/auth.ts +14 -1
- package/src/api/controllers/global/users.ts +6 -8
- package/src/api/controllers/system/accounts.ts +21 -0
- package/src/api/{index.js → index.ts} +12 -16
- package/src/api/routes/global/configs.js +1 -0
- package/src/api/routes/{tests/auth.spec.js → global/tests/auth.spec.ts} +32 -56
- package/src/api/routes/{tests/configs.spec.js → global/tests/configs.spec.ts} +76 -47
- package/src/api/routes/{tests/email.spec.js → global/tests/email.spec.ts} +7 -15
- package/src/api/routes/{tests/realEmail.spec.js → global/tests/realEmail.spec.ts} +20 -21
- package/src/api/routes/{tests/self.spec.js → global/tests/self.spec.ts} +10 -18
- package/src/api/routes/global/tests/users.spec.ts +464 -0
- package/src/api/routes/index.js +2 -0
- package/src/api/routes/system/accounts.ts +19 -0
- package/src/api/routes/system/tests/accounts.spec.ts +57 -0
- package/src/{environment.js → environment.ts} +10 -9
- package/src/index.ts +6 -4
- package/src/sdk/accounts/accounts.ts +53 -0
- package/src/sdk/accounts/index.ts +1 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/users/users.ts +217 -77
- package/src/tests/TestConfiguration.ts +273 -0
- package/src/tests/api/accounts.ts +28 -0
- package/src/tests/api/auth.ts +48 -0
- package/src/tests/api/configs.ts +40 -0
- package/src/tests/api/email.ts +24 -0
- package/src/tests/api/index.ts +25 -0
- package/src/tests/api/self.ts +21 -0
- package/src/tests/api/users.ts +95 -0
- package/src/tests/index.ts +14 -0
- package/src/tests/mocks/index.ts +7 -0
- package/src/tests/structures/accounts.ts +24 -0
- package/src/tests/structures/index.ts +20 -0
- package/src/tests/structures/users.ts +12 -4
- package/src/utilities/redis.js +1 -0
- package/scripts/load/users.js +0 -97
- package/src/api/routes/tests/users.spec.js +0 -390
- package/src/tests/TestConfiguration.js +0 -231
- package/src/tests/index.js +0 -12
- package/src/tests/mocks/index.js +0 -5
- package/src/tests/structures/index.js +0 -14
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { accounts } from "../../../../sdk"
|
|
2
|
+
import { TestConfiguration, structures, API } from "../../../../tests"
|
|
3
|
+
import { v4 as uuid } from "uuid"
|
|
4
|
+
|
|
5
|
+
describe("accounts", () => {
|
|
6
|
+
const config = new TestConfiguration()
|
|
7
|
+
const api = new API(config)
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await config.beforeAll()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await config.afterAll()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe("metadata", () => {
|
|
22
|
+
describe("saveMetadata", () => {
|
|
23
|
+
it("saves account metadata", async () => {
|
|
24
|
+
let account = structures.accounts.account()
|
|
25
|
+
|
|
26
|
+
const response = await api.accounts.saveMetadata(account)
|
|
27
|
+
|
|
28
|
+
const id = accounts.formatAccountMetadataId(account.accountId)
|
|
29
|
+
const metadata = await accounts.getMetadata(id)
|
|
30
|
+
expect(response).toStrictEqual(metadata)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe("destroyMetadata", () => {
|
|
35
|
+
it("destroys account metadata", async () => {
|
|
36
|
+
const account = structures.accounts.account()
|
|
37
|
+
await api.accounts.saveMetadata(account)
|
|
38
|
+
|
|
39
|
+
await api.accounts.destroyMetadata(account.accountId)
|
|
40
|
+
|
|
41
|
+
const deleted = await accounts.getMetadata(account.accountId)
|
|
42
|
+
expect(deleted).toBe(undefined)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("destroys account metadata that does not exist", async () => {
|
|
46
|
+
const id = uuid()
|
|
47
|
+
|
|
48
|
+
const response = await api.accounts.destroyMetadata(id)
|
|
49
|
+
|
|
50
|
+
expect(response.status).toBe(404)
|
|
51
|
+
expect(response.body.message).toBe(
|
|
52
|
+
`id=${accounts.formatAccountMetadataId(id)} does not exist`
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -20,13 +20,13 @@ if (!LOADED && isDev() && !isTest()) {
|
|
|
20
20
|
LOADED = true
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function parseIntSafe(number) {
|
|
23
|
+
function parseIntSafe(number: any) {
|
|
24
24
|
if (number) {
|
|
25
25
|
return parseInt(number)
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const env = {
|
|
30
30
|
// auth
|
|
31
31
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
|
32
32
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
|
@@ -47,7 +47,7 @@ module.exports = {
|
|
|
47
47
|
CLUSTER_PORT: process.env.CLUSTER_PORT,
|
|
48
48
|
// flags
|
|
49
49
|
NODE_ENV: process.env.NODE_ENV,
|
|
50
|
-
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
|
50
|
+
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
|
51
51
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
|
52
52
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
|
53
53
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
|
@@ -62,7 +62,7 @@ module.exports = {
|
|
|
62
62
|
// other
|
|
63
63
|
CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
|
|
64
64
|
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
|
|
65
|
-
_set(key, value) {
|
|
65
|
+
_set(key: any, value: any) {
|
|
66
66
|
process.env[key] = value
|
|
67
67
|
module.exports[key] = value
|
|
68
68
|
},
|
|
@@ -74,16 +74,17 @@ module.exports = {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// if some var haven't been set, define them
|
|
77
|
-
if (!
|
|
78
|
-
|
|
79
|
-
? "http://localhost:4001"
|
|
80
|
-
: "http://app-service:4002"
|
|
77
|
+
if (!env.APPS_URL) {
|
|
78
|
+
env.APPS_URL = isDev() ? "http://localhost:4001" : "http://app-service:4002"
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
// clean up any environment variable edge cases
|
|
84
82
|
for (let [key, value] of Object.entries(module.exports)) {
|
|
85
83
|
// handle the edge case of "0" to disable an environment variable
|
|
86
84
|
if (value === "0") {
|
|
87
|
-
|
|
85
|
+
// @ts-ignore
|
|
86
|
+
env[key] = 0
|
|
88
87
|
}
|
|
89
88
|
}
|
|
89
|
+
|
|
90
|
+
export = env
|
package/src/index.ts
CHANGED
|
@@ -71,9 +71,7 @@ server.on("close", async () => {
|
|
|
71
71
|
return
|
|
72
72
|
}
|
|
73
73
|
shuttingDown = true
|
|
74
|
-
|
|
75
|
-
console.log("Server Closed")
|
|
76
|
-
}
|
|
74
|
+
console.log("Server Closed")
|
|
77
75
|
await redis.shutdown()
|
|
78
76
|
await events.shutdown()
|
|
79
77
|
if (!env.isTest()) {
|
|
@@ -86,7 +84,7 @@ const shutdown = () => {
|
|
|
86
84
|
server.destroy()
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
export = server.listen(parseInt(env.PORT || 4002), async () => {
|
|
90
88
|
console.log(`Worker running on ${JSON.stringify(server.address())}`)
|
|
91
89
|
await redis.init()
|
|
92
90
|
})
|
|
@@ -100,3 +98,7 @@ process.on("uncaughtException", err => {
|
|
|
100
98
|
process.on("SIGTERM", () => {
|
|
101
99
|
shutdown()
|
|
102
100
|
})
|
|
101
|
+
|
|
102
|
+
process.on("SIGINT", () => {
|
|
103
|
+
shutdown()
|
|
104
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AccountMetadata } from "@budibase/types"
|
|
2
|
+
import {
|
|
3
|
+
db,
|
|
4
|
+
StaticDatabases,
|
|
5
|
+
HTTPError,
|
|
6
|
+
DocumentType,
|
|
7
|
+
SEPARATOR,
|
|
8
|
+
} from "@budibase/backend-core"
|
|
9
|
+
|
|
10
|
+
export const formatAccountMetadataId = (accountId: string) => {
|
|
11
|
+
return `${DocumentType.ACCOUNT_METADATA}${SEPARATOR}${accountId}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const saveMetadata = async (
|
|
15
|
+
metadata: AccountMetadata
|
|
16
|
+
): Promise<AccountMetadata> => {
|
|
17
|
+
return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
|
|
18
|
+
const existing = await getMetadata(metadata._id!)
|
|
19
|
+
if (existing) {
|
|
20
|
+
metadata._rev = existing._rev
|
|
21
|
+
}
|
|
22
|
+
const res = await db.put(metadata)
|
|
23
|
+
metadata._rev = res.rev
|
|
24
|
+
return metadata
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getMetadata = async (
|
|
29
|
+
accountId: string
|
|
30
|
+
): Promise<AccountMetadata | undefined> => {
|
|
31
|
+
return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
|
|
32
|
+
try {
|
|
33
|
+
return await db.get(accountId)
|
|
34
|
+
} catch (e: any) {
|
|
35
|
+
if (e.status === 404) {
|
|
36
|
+
// do nothing
|
|
37
|
+
return
|
|
38
|
+
} else {
|
|
39
|
+
throw e
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const destroyMetadata = async (accountId: string) => {
|
|
46
|
+
await db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
|
|
47
|
+
const metadata = await getMetadata(accountId)
|
|
48
|
+
if (!metadata) {
|
|
49
|
+
throw new HTTPError(`id=${accountId} does not exist`, 404)
|
|
50
|
+
}
|
|
51
|
+
await db.remove(accountId, metadata._rev)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./accounts"
|
package/src/sdk/index.ts
CHANGED
package/src/sdk/users/users.ts
CHANGED
|
@@ -14,8 +14,23 @@ import {
|
|
|
14
14
|
HTTPError,
|
|
15
15
|
accounts,
|
|
16
16
|
migrations,
|
|
17
|
+
StaticDatabases,
|
|
18
|
+
ViewName,
|
|
17
19
|
} from "@budibase/backend-core"
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
MigrationType,
|
|
22
|
+
PlatformUserByEmail,
|
|
23
|
+
User,
|
|
24
|
+
Account,
|
|
25
|
+
BulkCreateUsersResponse,
|
|
26
|
+
CreateUserResponse,
|
|
27
|
+
BulkDeleteUsersResponse,
|
|
28
|
+
CloudAccount,
|
|
29
|
+
AllDocsResponse,
|
|
30
|
+
RowResponse,
|
|
31
|
+
BulkDocsResponse,
|
|
32
|
+
AccountMetadata,
|
|
33
|
+
} from "@budibase/types"
|
|
19
34
|
import { groups as groupUtils } from "@budibase/pro"
|
|
20
35
|
|
|
21
36
|
const PAGE_LIMIT = 8
|
|
@@ -98,7 +113,6 @@ export const getUser = async (userId: string) => {
|
|
|
98
113
|
interface SaveUserOpts {
|
|
99
114
|
hashPassword?: boolean
|
|
100
115
|
requirePassword?: boolean
|
|
101
|
-
bulkCreate?: boolean
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
const buildUser = async (
|
|
@@ -109,7 +123,7 @@ const buildUser = async (
|
|
|
109
123
|
},
|
|
110
124
|
tenantId: string,
|
|
111
125
|
dbUser?: any
|
|
112
|
-
) => {
|
|
126
|
+
): Promise<User> => {
|
|
113
127
|
let { password, _id } = user
|
|
114
128
|
|
|
115
129
|
let hashedPassword
|
|
@@ -143,62 +157,63 @@ const buildUser = async (
|
|
|
143
157
|
return user
|
|
144
158
|
}
|
|
145
159
|
|
|
160
|
+
const validateUniqueUser = async (email: string, tenantId: string) => {
|
|
161
|
+
// check budibase users in other tenants
|
|
162
|
+
if (env.MULTI_TENANCY) {
|
|
163
|
+
const tenantUser = await tenancy.getTenantUser(email)
|
|
164
|
+
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
|
165
|
+
throw `Unavailable`
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// check root account users in account portal
|
|
170
|
+
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
171
|
+
const account = await accounts.getAccount(email)
|
|
172
|
+
if (account && account.verified && account.tenantId !== tenantId) {
|
|
173
|
+
throw `Unavailable`
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
146
178
|
export const save = async (
|
|
147
|
-
user:
|
|
179
|
+
user: User,
|
|
148
180
|
opts: SaveUserOpts = {
|
|
149
181
|
hashPassword: true,
|
|
150
182
|
requirePassword: true,
|
|
151
|
-
bulkCreate: false,
|
|
152
183
|
}
|
|
153
|
-
) => {
|
|
184
|
+
): Promise<CreateUserResponse> => {
|
|
154
185
|
const tenantId = tenancy.getTenantId()
|
|
155
186
|
const db = tenancy.getGlobalDB()
|
|
156
187
|
let { email, _id } = user
|
|
157
|
-
// make sure another user isn't using the same email
|
|
158
|
-
let dbUser: any
|
|
159
|
-
if (opts.bulkCreate) {
|
|
160
|
-
dbUser = null
|
|
161
|
-
} else if (email) {
|
|
162
|
-
// check budibase users inside the tenant
|
|
163
|
-
dbUser = await usersCore.getGlobalUserByEmail(email)
|
|
164
|
-
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
|
|
165
|
-
throw `Email address ${email} already in use.`
|
|
166
|
-
}
|
|
167
188
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
189
|
+
let dbUser: User | undefined
|
|
190
|
+
if (_id) {
|
|
191
|
+
// try to get existing user from db
|
|
192
|
+
dbUser = (await db.get(_id)) as User
|
|
193
|
+
if (email && dbUser.email !== email) {
|
|
194
|
+
throw "Email address cannot be changed"
|
|
174
195
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
196
|
+
email = dbUser.email
|
|
197
|
+
} else if (email) {
|
|
198
|
+
// no id was specified - load from email instead
|
|
199
|
+
dbUser = await usersCore.getGlobalUserByEmail(email)
|
|
200
|
+
if (dbUser && dbUser._id !== _id) {
|
|
201
|
+
throw `Unavailable`
|
|
182
202
|
}
|
|
183
|
-
} else
|
|
184
|
-
|
|
203
|
+
} else {
|
|
204
|
+
throw new Error("_id or email is required")
|
|
185
205
|
}
|
|
186
206
|
|
|
207
|
+
await validateUniqueUser(email, tenantId)
|
|
208
|
+
|
|
187
209
|
let builtUser = await buildUser(user, opts, tenantId, dbUser)
|
|
188
210
|
|
|
189
211
|
// make sure we set the _id field for a new user
|
|
190
212
|
if (!_id) {
|
|
191
|
-
_id = builtUser._id
|
|
213
|
+
_id = builtUser._id!
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
try {
|
|
195
|
-
const putOpts = {
|
|
196
|
-
password: builtUser.password,
|
|
197
|
-
...user,
|
|
198
|
-
}
|
|
199
|
-
if (opts.bulkCreate) {
|
|
200
|
-
return putOpts
|
|
201
|
-
}
|
|
202
217
|
// save the user to db
|
|
203
218
|
let response
|
|
204
219
|
const putUserFn = () => {
|
|
@@ -247,29 +262,87 @@ export const addTenant = async (
|
|
|
247
262
|
}
|
|
248
263
|
}
|
|
249
264
|
|
|
265
|
+
const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
|
|
266
|
+
return dbUtils.queryGlobalView(ViewName.USER_BY_EMAIL, {
|
|
267
|
+
keys: emails,
|
|
268
|
+
include_docs: true,
|
|
269
|
+
arrayResponse: true,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const getExistingPlatformUsers = async (
|
|
274
|
+
emails: string[]
|
|
275
|
+
): Promise<PlatformUserByEmail[]> => {
|
|
276
|
+
return dbUtils.doWithDB(
|
|
277
|
+
StaticDatabases.PLATFORM_INFO.name,
|
|
278
|
+
async (infoDb: any) => {
|
|
279
|
+
const response: AllDocsResponse<PlatformUserByEmail> =
|
|
280
|
+
await infoDb.allDocs({
|
|
281
|
+
keys: emails,
|
|
282
|
+
include_docs: true,
|
|
283
|
+
})
|
|
284
|
+
return response.rows
|
|
285
|
+
.filter(row => row.doc && (row.error !== "not_found") !== null)
|
|
286
|
+
.map((row: any) => row.doc)
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const getExistingAccounts = async (
|
|
292
|
+
emails: string[]
|
|
293
|
+
): Promise<AccountMetadata[]> => {
|
|
294
|
+
return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
|
|
295
|
+
keys: emails,
|
|
296
|
+
include_docs: true,
|
|
297
|
+
arrayResponse: true,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Apply a system-wide search on emails:
|
|
303
|
+
* - in tenant
|
|
304
|
+
* - cross tenant
|
|
305
|
+
* - accounts
|
|
306
|
+
* return an array of emails that match the supplied emails.
|
|
307
|
+
*/
|
|
308
|
+
const searchExistingEmails = async (emails: string[]) => {
|
|
309
|
+
let matchedEmails: string[] = []
|
|
310
|
+
|
|
311
|
+
const existingTenantUsers = await getExistingTenantUsers(emails)
|
|
312
|
+
matchedEmails.push(...existingTenantUsers.map(user => user.email))
|
|
313
|
+
|
|
314
|
+
const existingPlatformUsers = await getExistingPlatformUsers(emails)
|
|
315
|
+
matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
|
|
316
|
+
|
|
317
|
+
const existingAccounts = await getExistingAccounts(emails)
|
|
318
|
+
matchedEmails.push(...existingAccounts.map(account => account.email))
|
|
319
|
+
|
|
320
|
+
return [...new Set(matchedEmails)]
|
|
321
|
+
}
|
|
322
|
+
|
|
250
323
|
export const bulkCreate = async (
|
|
251
324
|
newUsersRequested: User[],
|
|
252
325
|
groups: string[]
|
|
253
|
-
) => {
|
|
326
|
+
): Promise<BulkCreateUsersResponse> => {
|
|
254
327
|
const db = tenancy.getGlobalDB()
|
|
255
328
|
const tenantId = tenancy.getTenantId()
|
|
256
329
|
|
|
257
330
|
let usersToSave: any[] = []
|
|
258
331
|
let newUsers: any[] = []
|
|
259
332
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
})
|
|
264
|
-
)
|
|
265
|
-
let mapped = allUsers.rows.map((row: any) => row.id)
|
|
333
|
+
const emails = newUsersRequested.map((user: User) => user.email)
|
|
334
|
+
const existingEmails = await searchExistingEmails(emails)
|
|
335
|
+
const unsuccessful: { email: string; reason: string }[] = []
|
|
266
336
|
|
|
267
|
-
const currentUserEmails = mapped.map((x: any) => x.email) || []
|
|
268
337
|
for (const newUser of newUsersRequested) {
|
|
269
338
|
if (
|
|
270
339
|
newUsers.find((x: any) => x.email === newUser.email) ||
|
|
271
|
-
|
|
340
|
+
existingEmails.includes(newUser.email)
|
|
272
341
|
) {
|
|
342
|
+
unsuccessful.push({
|
|
343
|
+
email: newUser.email,
|
|
344
|
+
reason: `Unavailable`,
|
|
345
|
+
})
|
|
273
346
|
continue
|
|
274
347
|
}
|
|
275
348
|
newUser.userGroups = groups
|
|
@@ -307,63 +380,130 @@ export const bulkCreate = async (
|
|
|
307
380
|
await apps.syncUserInApps(user._id)
|
|
308
381
|
}
|
|
309
382
|
|
|
310
|
-
|
|
383
|
+
const saved = usersToBulkSave.map(user => {
|
|
311
384
|
return {
|
|
312
385
|
_id: user._id,
|
|
313
386
|
email: user.email,
|
|
314
387
|
}
|
|
315
388
|
})
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
successful: saved,
|
|
392
|
+
unsuccessful,
|
|
393
|
+
}
|
|
316
394
|
}
|
|
317
395
|
|
|
318
|
-
|
|
396
|
+
/**
|
|
397
|
+
* For the given user id's, return the account holder if it is in the ids.
|
|
398
|
+
*/
|
|
399
|
+
const getAccountHolderFromUserIds = async (
|
|
400
|
+
userIds: string[]
|
|
401
|
+
): Promise<CloudAccount | undefined> => {
|
|
402
|
+
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
403
|
+
const tenantId = tenancy.getTenantId()
|
|
404
|
+
const account = await accounts.getAccountByTenantId(tenantId)
|
|
405
|
+
if (!account) {
|
|
406
|
+
throw new Error(`Account not found for tenantId=${tenantId}`)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const budibaseUserId = account.budibaseUserId
|
|
410
|
+
if (userIds.includes(budibaseUserId)) {
|
|
411
|
+
return account
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export const bulkDelete = async (
|
|
417
|
+
userIds: string[]
|
|
418
|
+
): Promise<BulkDeleteUsersResponse> => {
|
|
319
419
|
const db = tenancy.getGlobalDB()
|
|
320
420
|
|
|
421
|
+
const response: BulkDeleteUsersResponse = {
|
|
422
|
+
successful: [],
|
|
423
|
+
unsuccessful: [],
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// remove the account holder from the delete request if present
|
|
427
|
+
const account = await getAccountHolderFromUserIds(userIds)
|
|
428
|
+
if (account) {
|
|
429
|
+
userIds = userIds.filter(u => u !== account.budibaseUserId)
|
|
430
|
+
// mark user as unsuccessful
|
|
431
|
+
response.unsuccessful.push({
|
|
432
|
+
_id: account.budibaseUserId,
|
|
433
|
+
email: account.email,
|
|
434
|
+
reason: "Account holder cannot be deleted",
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
321
438
|
let groupsToModify: any = {}
|
|
322
439
|
let builderCount = 0
|
|
440
|
+
|
|
323
441
|
// Get users and delete
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
442
|
+
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
|
|
443
|
+
include_docs: true,
|
|
444
|
+
keys: userIds,
|
|
445
|
+
})
|
|
446
|
+
const usersToDelete: User[] = allDocsResponse.rows.map(
|
|
447
|
+
(user: RowResponse<User>) => {
|
|
448
|
+
// if we find a user that has an associated group, add it to
|
|
449
|
+
// an array so we can easily use allDocs on them later.
|
|
450
|
+
// This prevents us having to re-loop over all the users
|
|
451
|
+
if (user.doc.userGroups) {
|
|
452
|
+
for (let groupId of user.doc.userGroups) {
|
|
453
|
+
if (!Object.keys(groupsToModify).includes(groupId)) {
|
|
454
|
+
groupsToModify[groupId] = [user.id]
|
|
455
|
+
} else {
|
|
456
|
+
groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
|
|
457
|
+
}
|
|
339
458
|
}
|
|
340
459
|
}
|
|
341
|
-
}
|
|
342
460
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
461
|
+
// Also figure out how many builders are being deleted
|
|
462
|
+
if (eventHelpers.isAddingBuilder(user.doc, null)) {
|
|
463
|
+
builderCount++
|
|
464
|
+
}
|
|
347
465
|
|
|
348
|
-
|
|
349
|
-
|
|
466
|
+
return user.doc
|
|
467
|
+
}
|
|
468
|
+
)
|
|
350
469
|
|
|
351
|
-
|
|
352
|
-
|
|
470
|
+
// Delete from DB
|
|
471
|
+
const dbResponse: BulkDocsResponse = await db.bulkDocs(
|
|
472
|
+
usersToDelete.map(user => ({
|
|
353
473
|
...user,
|
|
354
474
|
_deleted: true,
|
|
355
475
|
}))
|
|
356
476
|
)
|
|
357
477
|
|
|
478
|
+
// Deletion post processing
|
|
358
479
|
await groupUtils.bulkDeleteGroupUsers(groupsToModify)
|
|
359
|
-
|
|
360
|
-
//Deletion post processing
|
|
361
480
|
for (let user of usersToDelete) {
|
|
362
481
|
await bulkDeleteProcessing(user)
|
|
363
482
|
}
|
|
364
|
-
|
|
365
483
|
await quotas.removeDevelopers(builderCount)
|
|
366
484
|
|
|
485
|
+
// Build Response
|
|
486
|
+
// index users by id
|
|
487
|
+
const userIndex: { [key: string]: User } = {}
|
|
488
|
+
usersToDelete.reduce((prev, current) => {
|
|
489
|
+
prev[current._id!] = current
|
|
490
|
+
return prev
|
|
491
|
+
}, userIndex)
|
|
492
|
+
|
|
493
|
+
// add the successful and unsuccessful users to response
|
|
494
|
+
dbResponse.forEach(item => {
|
|
495
|
+
const email = userIndex[item.id].email
|
|
496
|
+
if (item.ok) {
|
|
497
|
+
response.successful.push({ _id: item.id, email })
|
|
498
|
+
} else {
|
|
499
|
+
response.unsuccessful.push({
|
|
500
|
+
_id: item.id,
|
|
501
|
+
email,
|
|
502
|
+
reason: "Database error",
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
})
|
|
506
|
+
|
|
367
507
|
return response
|
|
368
508
|
}
|
|
369
509
|
|