@budibase/worker 2.3.18-alpha.15 → 2.3.18-alpha.17
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 +6 -6
- package/src/api/controllers/global/configs.ts +70 -1
- package/src/api/controllers/global/users.ts +149 -10
- package/src/api/routes/global/configs.ts +6 -6
- package/src/api/routes/global/tests/users.spec.ts +5 -1
- package/src/api/routes/global/users.ts +17 -12
- package/src/sdk/users/users.ts +17 -2
- package/src/utilities/email.ts +1 -1
- package/src/utilities/redis.ts +39 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "2.3.18-alpha.
|
|
4
|
+
"version": "2.3.18-alpha.17",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"author": "Budibase",
|
|
37
37
|
"license": "GPL-3.0",
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@budibase/backend-core": "2.3.18-alpha.
|
|
40
|
-
"@budibase/pro": "2.3.18-alpha.
|
|
41
|
-
"@budibase/string-templates": "2.3.18-alpha.
|
|
42
|
-
"@budibase/types": "2.3.18-alpha.
|
|
39
|
+
"@budibase/backend-core": "2.3.18-alpha.17",
|
|
40
|
+
"@budibase/pro": "2.3.18-alpha.16",
|
|
41
|
+
"@budibase/string-templates": "2.3.18-alpha.17",
|
|
42
|
+
"@budibase/types": "2.3.18-alpha.17",
|
|
43
43
|
"@koa/router": "8.0.8",
|
|
44
44
|
"@sentry/node": "6.17.7",
|
|
45
45
|
"@techpass/passport-openidconnect": "0.3.2",
|
|
@@ -101,5 +101,5 @@
|
|
|
101
101
|
"typescript": "4.7.3",
|
|
102
102
|
"update-dotenv": "1.1.1"
|
|
103
103
|
},
|
|
104
|
-
"gitHead": "
|
|
104
|
+
"gitHead": "f703ebb73409290aa949c240c51b95c7ce3826c8"
|
|
105
105
|
}
|
|
@@ -17,10 +17,15 @@ import {
|
|
|
17
17
|
Ctx,
|
|
18
18
|
GetPublicOIDCConfigResponse,
|
|
19
19
|
GetPublicSettingsResponse,
|
|
20
|
+
GoogleInnerConfig,
|
|
20
21
|
isGoogleConfig,
|
|
21
22
|
isOIDCConfig,
|
|
22
23
|
isSettingsConfig,
|
|
23
24
|
isSMTPConfig,
|
|
25
|
+
OIDCConfigs,
|
|
26
|
+
SettingsInnerConfig,
|
|
27
|
+
SSOConfig,
|
|
28
|
+
SSOConfigType,
|
|
24
29
|
UserCtx,
|
|
25
30
|
} from "@budibase/types"
|
|
26
31
|
import * as pro from "@budibase/pro"
|
|
@@ -119,6 +124,61 @@ const getEventFns = async (config: Config, existing?: Config) => {
|
|
|
119
124
|
return fns
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
type SSOConfigs = { [key in SSOConfigType]: SSOConfig | undefined }
|
|
128
|
+
|
|
129
|
+
async function getSSOConfigs(): Promise<SSOConfigs> {
|
|
130
|
+
const google = await configs.getGoogleConfig()
|
|
131
|
+
const oidc = await configs.getOIDCConfig()
|
|
132
|
+
return {
|
|
133
|
+
[ConfigType.GOOGLE]: google,
|
|
134
|
+
[ConfigType.OIDC]: oidc,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function hasActivatedConfig(ssoConfigs?: SSOConfigs) {
|
|
139
|
+
if (!ssoConfigs) {
|
|
140
|
+
ssoConfigs = await getSSOConfigs()
|
|
141
|
+
}
|
|
142
|
+
return !!Object.values(ssoConfigs).find(c => c?.activated)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function verifySettingsConfig(config: SettingsInnerConfig) {
|
|
146
|
+
if (config.isSSOEnforced) {
|
|
147
|
+
const valid = await hasActivatedConfig()
|
|
148
|
+
if (!valid) {
|
|
149
|
+
throw new Error("Cannot enforce SSO without an activated configuration")
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function verifySSOConfig(type: SSOConfigType, config: SSOConfig) {
|
|
155
|
+
const settings = await configs.getSettingsConfig()
|
|
156
|
+
if (settings.isSSOEnforced && !config.activated) {
|
|
157
|
+
// config is being saved as deactivated
|
|
158
|
+
// ensure there is at least one other activated sso config
|
|
159
|
+
const ssoConfigs = await getSSOConfigs()
|
|
160
|
+
|
|
161
|
+
// overwrite the config being updated
|
|
162
|
+
// to reflect the desired state
|
|
163
|
+
ssoConfigs[type] = config
|
|
164
|
+
|
|
165
|
+
const activated = await hasActivatedConfig(ssoConfigs)
|
|
166
|
+
if (!activated) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"Configuration cannot be deactivated while SSO is enforced"
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function verifyGoogleConfig(config: GoogleInnerConfig) {
|
|
175
|
+
await verifySSOConfig(ConfigType.GOOGLE, config)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function verifyOIDCConfig(config: OIDCConfigs) {
|
|
179
|
+
await verifySSOConfig(ConfigType.OIDC, config.configs[0])
|
|
180
|
+
}
|
|
181
|
+
|
|
122
182
|
export async function save(ctx: UserCtx<Config>) {
|
|
123
183
|
const body = ctx.request.body
|
|
124
184
|
const type = body.type
|
|
@@ -133,10 +193,19 @@ export async function save(ctx: UserCtx<Config>) {
|
|
|
133
193
|
|
|
134
194
|
try {
|
|
135
195
|
// verify the configuration
|
|
136
|
-
switch (
|
|
196
|
+
switch (type) {
|
|
137
197
|
case ConfigType.SMTP:
|
|
138
198
|
await email.verifyConfig(config)
|
|
139
199
|
break
|
|
200
|
+
case ConfigType.SETTINGS:
|
|
201
|
+
await verifySettingsConfig(config)
|
|
202
|
+
break
|
|
203
|
+
case ConfigType.GOOGLE:
|
|
204
|
+
await verifyGoogleConfig(config)
|
|
205
|
+
break
|
|
206
|
+
case ConfigType.OIDC:
|
|
207
|
+
await verifyOIDCConfig(config)
|
|
208
|
+
break
|
|
140
209
|
}
|
|
141
210
|
} catch (err: any) {
|
|
142
211
|
ctx.throw(400, err)
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
checkInviteCode,
|
|
3
|
+
getInviteCodes,
|
|
4
|
+
updateInviteCode,
|
|
5
|
+
} from "../../../utilities/redis"
|
|
6
|
+
// import sdk from "../../../sdk"
|
|
2
7
|
import * as userSdk from "../../../sdk/users"
|
|
3
8
|
import env from "../../../environment"
|
|
4
9
|
import {
|
|
@@ -28,6 +33,7 @@ import {
|
|
|
28
33
|
platform,
|
|
29
34
|
} from "@budibase/backend-core"
|
|
30
35
|
import { checkAnyUserExists } from "../../../utilities/users"
|
|
36
|
+
import { isEmailConfigured } from "../../../utilities/email"
|
|
31
37
|
|
|
32
38
|
const MAX_USERS_UPLOAD_LIMIT = 1000
|
|
33
39
|
|
|
@@ -179,16 +185,28 @@ export const destroy = async (ctx: any) => {
|
|
|
179
185
|
}
|
|
180
186
|
}
|
|
181
187
|
|
|
188
|
+
export const getAppUsers = async (ctx: any) => {
|
|
189
|
+
const body = ctx.request.body as SearchUsersRequest
|
|
190
|
+
const users = await userSdk.getUsersByAppAccess(body?.appId)
|
|
191
|
+
|
|
192
|
+
ctx.body = { data: users }
|
|
193
|
+
}
|
|
194
|
+
|
|
182
195
|
export const search = async (ctx: any) => {
|
|
183
196
|
const body = ctx.request.body as SearchUsersRequest
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
197
|
+
|
|
198
|
+
if (body.paginated === false) {
|
|
199
|
+
await getAppUsers(ctx)
|
|
200
|
+
} else {
|
|
201
|
+
const paginated = await userSdk.paginatedUsers(body)
|
|
202
|
+
// user hashed password shouldn't ever be returned
|
|
203
|
+
for (let user of paginated.data) {
|
|
204
|
+
if (user) {
|
|
205
|
+
delete user.password
|
|
206
|
+
}
|
|
189
207
|
}
|
|
208
|
+
ctx.body = paginated
|
|
190
209
|
}
|
|
191
|
-
ctx.body = paginated
|
|
192
210
|
}
|
|
193
211
|
|
|
194
212
|
// called internally by app server user fetch
|
|
@@ -218,9 +236,71 @@ export const tenantUserLookup = async (ctx: any) => {
|
|
|
218
236
|
}
|
|
219
237
|
}
|
|
220
238
|
|
|
239
|
+
/*
|
|
240
|
+
Encapsulate the app user onboarding flows here.
|
|
241
|
+
*/
|
|
242
|
+
export const onboardUsers = async (ctx: any) => {
|
|
243
|
+
const request = ctx.request.body as InviteUsersRequest | BulkUserRequest
|
|
244
|
+
const isBulkCreate = "create" in request
|
|
245
|
+
|
|
246
|
+
const emailConfigured = await isEmailConfigured()
|
|
247
|
+
|
|
248
|
+
let onboardingResponse
|
|
249
|
+
|
|
250
|
+
if (isBulkCreate) {
|
|
251
|
+
// @ts-ignore
|
|
252
|
+
const { users, groups, roles } = request.create
|
|
253
|
+
const assignUsers = users.map((user: User) => (user.roles = roles))
|
|
254
|
+
onboardingResponse = await userSdk.bulkCreate(assignUsers, groups)
|
|
255
|
+
ctx.body = onboardingResponse
|
|
256
|
+
} else if (emailConfigured) {
|
|
257
|
+
onboardingResponse = await inviteMultiple(ctx)
|
|
258
|
+
} else if (!emailConfigured) {
|
|
259
|
+
const inviteRequest = ctx.request.body as InviteUsersRequest
|
|
260
|
+
|
|
261
|
+
let createdPasswords: any = {}
|
|
262
|
+
|
|
263
|
+
const users: User[] = inviteRequest.map(invite => {
|
|
264
|
+
let password = Math.random().toString(36).substring(2, 22)
|
|
265
|
+
|
|
266
|
+
// Temp password to be passed to the user.
|
|
267
|
+
createdPasswords[invite.email] = password
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
email: invite.email,
|
|
271
|
+
password,
|
|
272
|
+
forceResetPassword: true,
|
|
273
|
+
roles: invite.userInfo.apps,
|
|
274
|
+
admin: { global: false },
|
|
275
|
+
builder: { global: false },
|
|
276
|
+
tenantId: tenancy.getTenantId(),
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
let bulkCreateReponse = await userSdk.bulkCreate(users, [])
|
|
280
|
+
|
|
281
|
+
// Apply temporary credentials
|
|
282
|
+
let createWithCredentials = {
|
|
283
|
+
...bulkCreateReponse,
|
|
284
|
+
successful: bulkCreateReponse?.successful.map(user => {
|
|
285
|
+
return {
|
|
286
|
+
...user,
|
|
287
|
+
password: createdPasswords[user.email],
|
|
288
|
+
}
|
|
289
|
+
}),
|
|
290
|
+
created: true,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
ctx.body = createWithCredentials
|
|
294
|
+
} else {
|
|
295
|
+
ctx.throw(400, "User onboarding failed")
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
221
299
|
export const invite = async (ctx: any) => {
|
|
222
300
|
const request = ctx.request.body as InviteUserRequest
|
|
223
|
-
|
|
301
|
+
|
|
302
|
+
let multiRequest = [request] as InviteUsersRequest
|
|
303
|
+
const response = await userSdk.invite(multiRequest)
|
|
224
304
|
|
|
225
305
|
// explicitly throw for single user invite
|
|
226
306
|
if (response.unsuccessful.length) {
|
|
@@ -234,6 +314,8 @@ export const invite = async (ctx: any) => {
|
|
|
234
314
|
|
|
235
315
|
ctx.body = {
|
|
236
316
|
message: "Invitation has been sent.",
|
|
317
|
+
successful: response.successful,
|
|
318
|
+
unsuccessful: response.unsuccessful,
|
|
237
319
|
}
|
|
238
320
|
}
|
|
239
321
|
|
|
@@ -255,6 +337,53 @@ export const checkInvite = async (ctx: any) => {
|
|
|
255
337
|
}
|
|
256
338
|
}
|
|
257
339
|
|
|
340
|
+
export const getUserInvites = async (ctx: any) => {
|
|
341
|
+
let invites
|
|
342
|
+
try {
|
|
343
|
+
// Restricted to the currently authenticated tenant
|
|
344
|
+
invites = await getInviteCodes([ctx.user.tenantId])
|
|
345
|
+
} catch (e) {
|
|
346
|
+
ctx.throw(400, "There was a problem fetching invites")
|
|
347
|
+
}
|
|
348
|
+
ctx.body = invites
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export const updateInvite = async (ctx: any) => {
|
|
352
|
+
const { code } = ctx.params
|
|
353
|
+
let updateBody = { ...ctx.request.body }
|
|
354
|
+
|
|
355
|
+
delete updateBody.email
|
|
356
|
+
|
|
357
|
+
let invite
|
|
358
|
+
try {
|
|
359
|
+
invite = await checkInviteCode(code, false)
|
|
360
|
+
if (!invite) {
|
|
361
|
+
throw new Error("The invite could not be retrieved")
|
|
362
|
+
}
|
|
363
|
+
} catch (e) {
|
|
364
|
+
ctx.throw(400, "There was a problem with the invite")
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let updated = {
|
|
368
|
+
...invite,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!updateBody?.apps || !Object.keys(updateBody?.apps).length) {
|
|
372
|
+
updated.info.apps = []
|
|
373
|
+
} else {
|
|
374
|
+
updated.info = {
|
|
375
|
+
...invite.info,
|
|
376
|
+
apps: {
|
|
377
|
+
...invite.info.apps,
|
|
378
|
+
...updateBody.apps,
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
await updateInviteCode(code, updated)
|
|
384
|
+
ctx.body = { ...invite }
|
|
385
|
+
}
|
|
386
|
+
|
|
258
387
|
export const inviteAccept = async (
|
|
259
388
|
ctx: Ctx<AcceptUserInviteRequest, AcceptUserInviteResponse>
|
|
260
389
|
) => {
|
|
@@ -263,13 +392,23 @@ export const inviteAccept = async (
|
|
|
263
392
|
// info is an extension of the user object that was stored by global
|
|
264
393
|
const { email, info }: any = await checkInviteCode(inviteCode)
|
|
265
394
|
const user = await tenancy.doInTenant(info.tenantId, async () => {
|
|
266
|
-
|
|
395
|
+
let request = {
|
|
267
396
|
firstName,
|
|
268
397
|
lastName,
|
|
269
398
|
password,
|
|
270
399
|
email,
|
|
400
|
+
roles: info.apps,
|
|
401
|
+
tenantId: info.tenantId,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
delete info.apps
|
|
405
|
+
|
|
406
|
+
request = {
|
|
407
|
+
...request,
|
|
271
408
|
...info,
|
|
272
|
-
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const saved = await userSdk.save(request)
|
|
273
412
|
const db = tenancy.getGlobalDB()
|
|
274
413
|
const user = await db.get(saved._id)
|
|
275
414
|
await events.user.inviteAccepted(user)
|
|
@@ -34,8 +34,8 @@ function settingValidation() {
|
|
|
34
34
|
function googleValidation() {
|
|
35
35
|
// prettier-ignore
|
|
36
36
|
return Joi.object({
|
|
37
|
-
clientID: Joi.
|
|
38
|
-
clientSecret: Joi.
|
|
37
|
+
clientID: Joi.string().required(),
|
|
38
|
+
clientSecret: Joi.string().required(),
|
|
39
39
|
activated: Joi.boolean().required(),
|
|
40
40
|
}).unknown(true)
|
|
41
41
|
}
|
|
@@ -45,12 +45,12 @@ function oidcValidation() {
|
|
|
45
45
|
return Joi.object({
|
|
46
46
|
configs: Joi.array().items(
|
|
47
47
|
Joi.object({
|
|
48
|
-
clientID: Joi.
|
|
49
|
-
clientSecret: Joi.
|
|
50
|
-
configUrl: Joi.
|
|
48
|
+
clientID: Joi.string().required(),
|
|
49
|
+
clientSecret: Joi.string().required(),
|
|
50
|
+
configUrl: Joi.string().required(),
|
|
51
51
|
logo: Joi.string().allow("", null),
|
|
52
52
|
name: Joi.string().allow("", null),
|
|
53
|
-
uuid: Joi.
|
|
53
|
+
uuid: Joi.string().required(),
|
|
54
54
|
activated: Joi.boolean().required(),
|
|
55
55
|
scopes: Joi.array().optional()
|
|
56
56
|
})
|
|
@@ -30,7 +30,11 @@ describe("/api/global/users", () => {
|
|
|
30
30
|
email
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
expect(res.body).
|
|
33
|
+
expect(res.body?.message).toBe("Invitation has been sent.")
|
|
34
|
+
expect(res.body?.unsuccessful.length).toBe(0)
|
|
35
|
+
expect(res.body?.successful.length).toBe(1)
|
|
36
|
+
expect(res.body?.successful[0].email).toBe(email)
|
|
37
|
+
|
|
34
38
|
expect(sendMailMock).toHaveBeenCalled()
|
|
35
39
|
expect(code).toBeDefined()
|
|
36
40
|
expect(events.user.invited).toBeCalledTimes(1)
|
|
@@ -38,13 +38,6 @@ function buildInviteMultipleValidation() {
|
|
|
38
38
|
))
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function buildInviteLookupValidation() {
|
|
42
|
-
// prettier-ignore
|
|
43
|
-
return auth.joiValidator.params(Joi.object({
|
|
44
|
-
code: Joi.string().required()
|
|
45
|
-
}).unknown(true))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
41
|
const createUserAdminOnly = (ctx: any, next: any) => {
|
|
49
42
|
if (!ctx.request.body._id) {
|
|
50
43
|
return auth.adminOnly(ctx, next)
|
|
@@ -88,22 +81,34 @@ router
|
|
|
88
81
|
.get("/api/global/roles/:appId")
|
|
89
82
|
.post(
|
|
90
83
|
"/api/global/users/invite",
|
|
91
|
-
auth.
|
|
84
|
+
auth.builderOrAdmin,
|
|
92
85
|
buildInviteValidation(),
|
|
93
86
|
controller.invite
|
|
94
87
|
)
|
|
88
|
+
.post(
|
|
89
|
+
"/api/global/users/onboard",
|
|
90
|
+
auth.builderOrAdmin,
|
|
91
|
+
buildInviteMultipleValidation(),
|
|
92
|
+
controller.onboardUsers
|
|
93
|
+
)
|
|
95
94
|
.post(
|
|
96
95
|
"/api/global/users/multi/invite",
|
|
97
|
-
auth.
|
|
96
|
+
auth.builderOrAdmin,
|
|
98
97
|
buildInviteMultipleValidation(),
|
|
99
98
|
controller.inviteMultiple
|
|
100
99
|
)
|
|
101
100
|
|
|
102
101
|
// non-global endpoints
|
|
102
|
+
.get("/api/global/users/invite/:code", controller.checkInvite)
|
|
103
|
+
.post(
|
|
104
|
+
"/api/global/users/invite/update/:code",
|
|
105
|
+
auth.builderOrAdmin,
|
|
106
|
+
controller.updateInvite
|
|
107
|
+
)
|
|
103
108
|
.get(
|
|
104
|
-
"/api/global/users/
|
|
105
|
-
|
|
106
|
-
controller.
|
|
109
|
+
"/api/global/users/invites",
|
|
110
|
+
auth.builderOrAdmin,
|
|
111
|
+
controller.getUserInvites
|
|
107
112
|
)
|
|
108
113
|
.post(
|
|
109
114
|
"/api/global/users/invite/accept",
|
package/src/sdk/users/users.ts
CHANGED
|
@@ -57,11 +57,22 @@ export const countUsersByApp = async (appId: string) => {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
export const getUsersByAppAccess = async (appId?: string) => {
|
|
61
|
+
const opts: any = {
|
|
62
|
+
include_docs: true,
|
|
63
|
+
limit: 50,
|
|
64
|
+
}
|
|
65
|
+
let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
|
|
66
|
+
appId,
|
|
67
|
+
opts
|
|
68
|
+
)
|
|
69
|
+
return response
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
export const paginatedUsers = async ({
|
|
61
73
|
page,
|
|
62
74
|
email,
|
|
63
75
|
appId,
|
|
64
|
-
userIds,
|
|
65
76
|
}: SearchUsersRequest = {}) => {
|
|
66
77
|
const db = tenancy.getGlobalDB()
|
|
67
78
|
// get one extra document, to have the next page
|
|
@@ -234,7 +245,7 @@ export const save = async (
|
|
|
234
245
|
const tenantId = tenancy.getTenantId()
|
|
235
246
|
const db = tenancy.getGlobalDB()
|
|
236
247
|
|
|
237
|
-
let { email, _id, userGroups = [] } = user
|
|
248
|
+
let { email, _id, userGroups = [], roles } = user
|
|
238
249
|
|
|
239
250
|
if (!email && !_id) {
|
|
240
251
|
throw new Error("_id or email is required")
|
|
@@ -276,6 +287,10 @@ export const save = async (
|
|
|
276
287
|
builtUser.roles = dbUser.roles
|
|
277
288
|
}
|
|
278
289
|
|
|
290
|
+
if (!dbUser && roles?.length) {
|
|
291
|
+
builtUser.roles = { ...roles }
|
|
292
|
+
}
|
|
293
|
+
|
|
279
294
|
// make sure we set the _id field for a new user
|
|
280
295
|
// Also if this is a new user, associate groups with them
|
|
281
296
|
let groupPromises = []
|
package/src/utilities/email.ts
CHANGED
|
@@ -203,7 +203,7 @@ export async function sendEmail(
|
|
|
203
203
|
* @param {object} config an SMTP configuration - this is based on the nodemailer API.
|
|
204
204
|
* @return {Promise<boolean>} returns true if the configuration is valid.
|
|
205
205
|
*/
|
|
206
|
-
export async function verifyConfig(config:
|
|
206
|
+
export async function verifyConfig(config: SMTPInnerConfig) {
|
|
207
207
|
const transport = createSMTPTransport(config)
|
|
208
208
|
await transport.verify()
|
|
209
209
|
}
|
package/src/utilities/redis.ts
CHANGED
|
@@ -7,7 +7,7 @@ function getExpirySecondsForDB(db: string) {
|
|
|
7
7
|
return 3600
|
|
8
8
|
case redis.utils.Databases.INVITATIONS:
|
|
9
9
|
// a day
|
|
10
|
-
return
|
|
10
|
+
return 604800
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -29,6 +29,20 @@ async function writeACode(db: string, value: any) {
|
|
|
29
29
|
return code
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
async function updateACode(db: string, code: string, value: any) {
|
|
33
|
+
const client = await getClient(db)
|
|
34
|
+
await client.store(code, value, getExpirySecondsForDB(db))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Given an invite code and invite body, allow the update an existing/valid invite in redis
|
|
39
|
+
* @param {string} inviteCode The invite code for an invite in redis
|
|
40
|
+
* @param {object} value The body of the updated user invitation
|
|
41
|
+
*/
|
|
42
|
+
export async function updateInviteCode(inviteCode: string, value: string) {
|
|
43
|
+
await updateACode(redis.utils.Databases.INVITATIONS, inviteCode, value)
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
async function getACode(db: string, code: string, deleteCode = true) {
|
|
33
47
|
const client = await getClient(db)
|
|
34
48
|
const value = await client.get(code)
|
|
@@ -113,3 +127,27 @@ export async function checkInviteCode(
|
|
|
113
127
|
throw "Invitation is not valid or has expired, please request a new one."
|
|
114
128
|
}
|
|
115
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
Get all currently available user invitations.
|
|
133
|
+
@return {Object[]} A list of all objects containing invite metadata
|
|
134
|
+
**/
|
|
135
|
+
export async function getInviteCodes(tenantIds?: string[]) {
|
|
136
|
+
const client = await getClient(redis.utils.Databases.INVITATIONS)
|
|
137
|
+
const invites: any[] = await client.scan()
|
|
138
|
+
|
|
139
|
+
const results = invites.map(invite => {
|
|
140
|
+
return {
|
|
141
|
+
...invite.value,
|
|
142
|
+
code: invite.key,
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
return results.reduce((acc, invite) => {
|
|
146
|
+
if (tenantIds?.length && tenantIds.includes(invite.info.tenantId)) {
|
|
147
|
+
acc.push(invite)
|
|
148
|
+
} else {
|
|
149
|
+
acc.push(invite)
|
|
150
|
+
}
|
|
151
|
+
return acc
|
|
152
|
+
}, [])
|
|
153
|
+
}
|