@budibase/worker 1.2.58 → 1.3.1
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 +26 -55
- 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 +512 -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 +271 -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 +111 -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,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,9 +14,28 @@ import {
|
|
|
14
14
|
HTTPError,
|
|
15
15
|
accounts,
|
|
16
16
|
migrations,
|
|
17
|
+
StaticDatabases,
|
|
18
|
+
ViewName,
|
|
19
|
+
events,
|
|
17
20
|
} from "@budibase/backend-core"
|
|
18
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
MigrationType,
|
|
23
|
+
PlatformUserByEmail,
|
|
24
|
+
User,
|
|
25
|
+
BulkCreateUsersResponse,
|
|
26
|
+
CreateUserResponse,
|
|
27
|
+
BulkDeleteUsersResponse,
|
|
28
|
+
CloudAccount,
|
|
29
|
+
AllDocsResponse,
|
|
30
|
+
RowResponse,
|
|
31
|
+
BulkDocsResponse,
|
|
32
|
+
AccountMetadata,
|
|
33
|
+
InviteUsersRequest,
|
|
34
|
+
InviteUsersResponse,
|
|
35
|
+
} from "@budibase/types"
|
|
19
36
|
import { groups as groupUtils } from "@budibase/pro"
|
|
37
|
+
import { sendEmail } from "../../utilities/email"
|
|
38
|
+
import { EmailTemplatePurpose } from "../../constants"
|
|
20
39
|
|
|
21
40
|
const PAGE_LIMIT = 8
|
|
22
41
|
|
|
@@ -98,7 +117,6 @@ export const getUser = async (userId: string) => {
|
|
|
98
117
|
interface SaveUserOpts {
|
|
99
118
|
hashPassword?: boolean
|
|
100
119
|
requirePassword?: boolean
|
|
101
|
-
bulkCreate?: boolean
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
const buildUser = async (
|
|
@@ -109,7 +127,7 @@ const buildUser = async (
|
|
|
109
127
|
},
|
|
110
128
|
tenantId: string,
|
|
111
129
|
dbUser?: any
|
|
112
|
-
) => {
|
|
130
|
+
): Promise<User> => {
|
|
113
131
|
let { password, _id } = user
|
|
114
132
|
|
|
115
133
|
let hashedPassword
|
|
@@ -143,62 +161,63 @@ const buildUser = async (
|
|
|
143
161
|
return user
|
|
144
162
|
}
|
|
145
163
|
|
|
164
|
+
const validateUniqueUser = async (email: string, tenantId: string) => {
|
|
165
|
+
// check budibase users in other tenants
|
|
166
|
+
if (env.MULTI_TENANCY) {
|
|
167
|
+
const tenantUser = await tenancy.getTenantUser(email)
|
|
168
|
+
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
|
169
|
+
throw `Unavailable`
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// check root account users in account portal
|
|
174
|
+
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
175
|
+
const account = await accounts.getAccount(email)
|
|
176
|
+
if (account && account.verified && account.tenantId !== tenantId) {
|
|
177
|
+
throw `Unavailable`
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
146
182
|
export const save = async (
|
|
147
|
-
user:
|
|
183
|
+
user: User,
|
|
148
184
|
opts: SaveUserOpts = {
|
|
149
185
|
hashPassword: true,
|
|
150
186
|
requirePassword: true,
|
|
151
|
-
bulkCreate: false,
|
|
152
187
|
}
|
|
153
|
-
) => {
|
|
188
|
+
): Promise<CreateUserResponse> => {
|
|
154
189
|
const tenantId = tenancy.getTenantId()
|
|
155
190
|
const db = tenancy.getGlobalDB()
|
|
156
191
|
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
192
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
let dbUser: User | undefined
|
|
194
|
+
if (_id) {
|
|
195
|
+
// try to get existing user from db
|
|
196
|
+
dbUser = (await db.get(_id)) as User
|
|
197
|
+
if (email && dbUser.email !== email) {
|
|
198
|
+
throw "Email address cannot be changed"
|
|
174
199
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
200
|
+
email = dbUser.email
|
|
201
|
+
} else if (email) {
|
|
202
|
+
// no id was specified - load from email instead
|
|
203
|
+
dbUser = await usersCore.getGlobalUserByEmail(email)
|
|
204
|
+
if (dbUser && dbUser._id !== _id) {
|
|
205
|
+
throw `Unavailable`
|
|
182
206
|
}
|
|
183
|
-
} else
|
|
184
|
-
|
|
207
|
+
} else {
|
|
208
|
+
throw new Error("_id or email is required")
|
|
185
209
|
}
|
|
186
210
|
|
|
211
|
+
await validateUniqueUser(email, tenantId)
|
|
212
|
+
|
|
187
213
|
let builtUser = await buildUser(user, opts, tenantId, dbUser)
|
|
188
214
|
|
|
189
215
|
// make sure we set the _id field for a new user
|
|
190
216
|
if (!_id) {
|
|
191
|
-
_id = builtUser._id
|
|
217
|
+
_id = builtUser._id!
|
|
192
218
|
}
|
|
193
219
|
|
|
194
220
|
try {
|
|
195
|
-
const putOpts = {
|
|
196
|
-
password: builtUser.password,
|
|
197
|
-
...user,
|
|
198
|
-
}
|
|
199
|
-
if (opts.bulkCreate) {
|
|
200
|
-
return putOpts
|
|
201
|
-
}
|
|
202
221
|
// save the user to db
|
|
203
222
|
let response
|
|
204
223
|
const putUserFn = () => {
|
|
@@ -247,29 +266,87 @@ export const addTenant = async (
|
|
|
247
266
|
}
|
|
248
267
|
}
|
|
249
268
|
|
|
269
|
+
const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
|
|
270
|
+
return dbUtils.queryGlobalView(ViewName.USER_BY_EMAIL, {
|
|
271
|
+
keys: emails,
|
|
272
|
+
include_docs: true,
|
|
273
|
+
arrayResponse: true,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const getExistingPlatformUsers = async (
|
|
278
|
+
emails: string[]
|
|
279
|
+
): Promise<PlatformUserByEmail[]> => {
|
|
280
|
+
return dbUtils.doWithDB(
|
|
281
|
+
StaticDatabases.PLATFORM_INFO.name,
|
|
282
|
+
async (infoDb: any) => {
|
|
283
|
+
const response: AllDocsResponse<PlatformUserByEmail> =
|
|
284
|
+
await infoDb.allDocs({
|
|
285
|
+
keys: emails,
|
|
286
|
+
include_docs: true,
|
|
287
|
+
})
|
|
288
|
+
return response.rows
|
|
289
|
+
.filter(row => row.doc && (row.error !== "not_found") !== null)
|
|
290
|
+
.map((row: any) => row.doc)
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const getExistingAccounts = async (
|
|
296
|
+
emails: string[]
|
|
297
|
+
): Promise<AccountMetadata[]> => {
|
|
298
|
+
return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
|
|
299
|
+
keys: emails,
|
|
300
|
+
include_docs: true,
|
|
301
|
+
arrayResponse: true,
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Apply a system-wide search on emails:
|
|
307
|
+
* - in tenant
|
|
308
|
+
* - cross tenant
|
|
309
|
+
* - accounts
|
|
310
|
+
* return an array of emails that match the supplied emails.
|
|
311
|
+
*/
|
|
312
|
+
const searchExistingEmails = async (emails: string[]) => {
|
|
313
|
+
let matchedEmails: string[] = []
|
|
314
|
+
|
|
315
|
+
const existingTenantUsers = await getExistingTenantUsers(emails)
|
|
316
|
+
matchedEmails.push(...existingTenantUsers.map(user => user.email))
|
|
317
|
+
|
|
318
|
+
const existingPlatformUsers = await getExistingPlatformUsers(emails)
|
|
319
|
+
matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
|
|
320
|
+
|
|
321
|
+
const existingAccounts = await getExistingAccounts(emails)
|
|
322
|
+
matchedEmails.push(...existingAccounts.map(account => account.email))
|
|
323
|
+
|
|
324
|
+
return [...new Set(matchedEmails)]
|
|
325
|
+
}
|
|
326
|
+
|
|
250
327
|
export const bulkCreate = async (
|
|
251
328
|
newUsersRequested: User[],
|
|
252
329
|
groups: string[]
|
|
253
|
-
) => {
|
|
330
|
+
): Promise<BulkCreateUsersResponse> => {
|
|
254
331
|
const db = tenancy.getGlobalDB()
|
|
255
332
|
const tenantId = tenancy.getTenantId()
|
|
256
333
|
|
|
257
334
|
let usersToSave: any[] = []
|
|
258
335
|
let newUsers: any[] = []
|
|
259
336
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
})
|
|
264
|
-
)
|
|
265
|
-
let mapped = allUsers.rows.map((row: any) => row.id)
|
|
337
|
+
const emails = newUsersRequested.map((user: User) => user.email)
|
|
338
|
+
const existingEmails = await searchExistingEmails(emails)
|
|
339
|
+
const unsuccessful: { email: string; reason: string }[] = []
|
|
266
340
|
|
|
267
|
-
const currentUserEmails = mapped.map((x: any) => x.email) || []
|
|
268
341
|
for (const newUser of newUsersRequested) {
|
|
269
342
|
if (
|
|
270
343
|
newUsers.find((x: any) => x.email === newUser.email) ||
|
|
271
|
-
|
|
344
|
+
existingEmails.includes(newUser.email)
|
|
272
345
|
) {
|
|
346
|
+
unsuccessful.push({
|
|
347
|
+
email: newUser.email,
|
|
348
|
+
reason: `Unavailable`,
|
|
349
|
+
})
|
|
273
350
|
continue
|
|
274
351
|
}
|
|
275
352
|
newUser.userGroups = groups
|
|
@@ -307,63 +384,130 @@ export const bulkCreate = async (
|
|
|
307
384
|
await apps.syncUserInApps(user._id)
|
|
308
385
|
}
|
|
309
386
|
|
|
310
|
-
|
|
387
|
+
const saved = usersToBulkSave.map(user => {
|
|
311
388
|
return {
|
|
312
389
|
_id: user._id,
|
|
313
390
|
email: user.email,
|
|
314
391
|
}
|
|
315
392
|
})
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
successful: saved,
|
|
396
|
+
unsuccessful,
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* For the given user id's, return the account holder if it is in the ids.
|
|
402
|
+
*/
|
|
403
|
+
const getAccountHolderFromUserIds = async (
|
|
404
|
+
userIds: string[]
|
|
405
|
+
): Promise<CloudAccount | undefined> => {
|
|
406
|
+
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
407
|
+
const tenantId = tenancy.getTenantId()
|
|
408
|
+
const account = await accounts.getAccountByTenantId(tenantId)
|
|
409
|
+
if (!account) {
|
|
410
|
+
throw new Error(`Account not found for tenantId=${tenantId}`)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const budibaseUserId = account.budibaseUserId
|
|
414
|
+
if (userIds.includes(budibaseUserId)) {
|
|
415
|
+
return account
|
|
416
|
+
}
|
|
417
|
+
}
|
|
316
418
|
}
|
|
317
419
|
|
|
318
|
-
export const bulkDelete = async (
|
|
420
|
+
export const bulkDelete = async (
|
|
421
|
+
userIds: string[]
|
|
422
|
+
): Promise<BulkDeleteUsersResponse> => {
|
|
319
423
|
const db = tenancy.getGlobalDB()
|
|
320
424
|
|
|
425
|
+
const response: BulkDeleteUsersResponse = {
|
|
426
|
+
successful: [],
|
|
427
|
+
unsuccessful: [],
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// remove the account holder from the delete request if present
|
|
431
|
+
const account = await getAccountHolderFromUserIds(userIds)
|
|
432
|
+
if (account) {
|
|
433
|
+
userIds = userIds.filter(u => u !== account.budibaseUserId)
|
|
434
|
+
// mark user as unsuccessful
|
|
435
|
+
response.unsuccessful.push({
|
|
436
|
+
_id: account.budibaseUserId,
|
|
437
|
+
email: account.email,
|
|
438
|
+
reason: "Account holder cannot be deleted",
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
321
442
|
let groupsToModify: any = {}
|
|
322
443
|
let builderCount = 0
|
|
444
|
+
|
|
323
445
|
// Get users and delete
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
446
|
+
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
|
|
447
|
+
include_docs: true,
|
|
448
|
+
keys: userIds,
|
|
449
|
+
})
|
|
450
|
+
const usersToDelete: User[] = allDocsResponse.rows.map(
|
|
451
|
+
(user: RowResponse<User>) => {
|
|
452
|
+
// if we find a user that has an associated group, add it to
|
|
453
|
+
// an array so we can easily use allDocs on them later.
|
|
454
|
+
// This prevents us having to re-loop over all the users
|
|
455
|
+
if (user.doc.userGroups) {
|
|
456
|
+
for (let groupId of user.doc.userGroups) {
|
|
457
|
+
if (!Object.keys(groupsToModify).includes(groupId)) {
|
|
458
|
+
groupsToModify[groupId] = [user.id]
|
|
459
|
+
} else {
|
|
460
|
+
groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
|
|
461
|
+
}
|
|
339
462
|
}
|
|
340
463
|
}
|
|
341
|
-
}
|
|
342
464
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
465
|
+
// Also figure out how many builders are being deleted
|
|
466
|
+
if (eventHelpers.isAddingBuilder(user.doc, null)) {
|
|
467
|
+
builderCount++
|
|
468
|
+
}
|
|
347
469
|
|
|
348
|
-
|
|
349
|
-
|
|
470
|
+
return user.doc
|
|
471
|
+
}
|
|
472
|
+
)
|
|
350
473
|
|
|
351
|
-
|
|
352
|
-
|
|
474
|
+
// Delete from DB
|
|
475
|
+
const dbResponse: BulkDocsResponse = await db.bulkDocs(
|
|
476
|
+
usersToDelete.map(user => ({
|
|
353
477
|
...user,
|
|
354
478
|
_deleted: true,
|
|
355
479
|
}))
|
|
356
480
|
)
|
|
357
481
|
|
|
482
|
+
// Deletion post processing
|
|
358
483
|
await groupUtils.bulkDeleteGroupUsers(groupsToModify)
|
|
359
|
-
|
|
360
|
-
//Deletion post processing
|
|
361
484
|
for (let user of usersToDelete) {
|
|
362
485
|
await bulkDeleteProcessing(user)
|
|
363
486
|
}
|
|
364
|
-
|
|
365
487
|
await quotas.removeDevelopers(builderCount)
|
|
366
488
|
|
|
489
|
+
// Build Response
|
|
490
|
+
// index users by id
|
|
491
|
+
const userIndex: { [key: string]: User } = {}
|
|
492
|
+
usersToDelete.reduce((prev, current) => {
|
|
493
|
+
prev[current._id!] = current
|
|
494
|
+
return prev
|
|
495
|
+
}, userIndex)
|
|
496
|
+
|
|
497
|
+
// add the successful and unsuccessful users to response
|
|
498
|
+
dbResponse.forEach(item => {
|
|
499
|
+
const email = userIndex[item.id].email
|
|
500
|
+
if (item.ok) {
|
|
501
|
+
response.successful.push({ _id: item.id, email })
|
|
502
|
+
} else {
|
|
503
|
+
response.unsuccessful.push({
|
|
504
|
+
_id: item.id,
|
|
505
|
+
email,
|
|
506
|
+
reason: "Database error",
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
|
|
367
511
|
return response
|
|
368
512
|
}
|
|
369
513
|
|
|
@@ -411,3 +555,53 @@ const bulkDeleteProcessing = async (dbUser: User) => {
|
|
|
411
555
|
// let server know to sync user
|
|
412
556
|
await apps.syncUserInApps(userId)
|
|
413
557
|
}
|
|
558
|
+
|
|
559
|
+
export const invite = async (
|
|
560
|
+
users: InviteUsersRequest
|
|
561
|
+
): Promise<InviteUsersResponse> => {
|
|
562
|
+
const response: InviteUsersResponse = {
|
|
563
|
+
successful: [],
|
|
564
|
+
unsuccessful: [],
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const matchedEmails = await searchExistingEmails(users.map(u => u.email))
|
|
568
|
+
const newUsers = []
|
|
569
|
+
|
|
570
|
+
// separate duplicates from new users
|
|
571
|
+
for (let user of users) {
|
|
572
|
+
if (matchedEmails.includes(user.email)) {
|
|
573
|
+
response.unsuccessful.push({ email: user.email, reason: "Unavailable" })
|
|
574
|
+
} else {
|
|
575
|
+
newUsers.push(user)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// overwrite users with new only
|
|
579
|
+
users = newUsers
|
|
580
|
+
|
|
581
|
+
// send the emails for new users
|
|
582
|
+
const tenantId = tenancy.getTenantId()
|
|
583
|
+
for (let user of users) {
|
|
584
|
+
try {
|
|
585
|
+
let userInfo = user.userInfo
|
|
586
|
+
if (!userInfo) {
|
|
587
|
+
userInfo = {}
|
|
588
|
+
}
|
|
589
|
+
userInfo.tenantId = tenantId
|
|
590
|
+
const opts: any = {
|
|
591
|
+
subject: "{{ company }} platform invitation",
|
|
592
|
+
info: userInfo,
|
|
593
|
+
}
|
|
594
|
+
await sendEmail(user.email, EmailTemplatePurpose.INVITATION, opts)
|
|
595
|
+
response.successful.push({ email: user.email })
|
|
596
|
+
await events.user.invited()
|
|
597
|
+
} catch (e) {
|
|
598
|
+
console.error(`Failed to send email invitation email=${user.email}`, e)
|
|
599
|
+
response.unsuccessful.push({
|
|
600
|
+
email: user.email,
|
|
601
|
+
reason: "Failed to send email",
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return response
|
|
607
|
+
}
|