@budibase/worker 2.3.17-alpha.3 → 2.3.17-alpha.5

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.
Files changed (43) hide show
  1. package/package.json +8 -7
  2. package/scripts/dev/manage.js +1 -0
  3. package/src/api/controllers/global/auth.ts +77 -70
  4. package/src/api/controllers/global/self.ts +28 -44
  5. package/src/api/controllers/global/users.ts +65 -25
  6. package/src/api/controllers/system/accounts.ts +7 -5
  7. package/src/api/controllers/system/tenants.ts +4 -8
  8. package/src/api/index.ts +4 -20
  9. package/src/api/routes/global/auth.ts +10 -7
  10. package/src/api/routes/global/tests/auth.spec.ts +234 -33
  11. package/src/api/routes/global/tests/configs.spec.ts +93 -113
  12. package/src/api/routes/global/tests/roles.spec.ts +3 -2
  13. package/src/api/routes/global/tests/self.spec.ts +3 -4
  14. package/src/api/routes/global/tests/users.spec.ts +44 -46
  15. package/src/api/routes/system/tenants.ts +1 -1
  16. package/src/api/routes/system/tests/accounts.spec.ts +4 -4
  17. package/src/api/routes/system/tests/migrations.spec.ts +2 -2
  18. package/src/api/routes/system/tests/restore.spec.ts +2 -2
  19. package/src/api/routes/system/tests/status.spec.ts +5 -5
  20. package/src/environment.ts +15 -1
  21. package/src/index.ts +6 -0
  22. package/src/middleware/tests/tenancy.spec.ts +4 -4
  23. package/src/migrations/functions/globalInfoSyncUsers.ts +4 -3
  24. package/src/sdk/accounts/index.ts +2 -1
  25. package/src/sdk/accounts/{accounts.ts → metadata.ts} +0 -1
  26. package/src/sdk/auth/auth.ts +86 -0
  27. package/src/sdk/auth/index.ts +1 -0
  28. package/src/sdk/tenants/index.ts +1 -0
  29. package/src/sdk/tenants/tenants.ts +76 -0
  30. package/src/sdk/users/events.ts +4 -0
  31. package/src/sdk/users/index.ts +1 -0
  32. package/src/sdk/users/tests/users.spec.ts +52 -0
  33. package/src/sdk/users/users.ts +53 -46
  34. package/src/tests/TestConfiguration.ts +38 -89
  35. package/src/tests/api/auth.ts +42 -18
  36. package/src/tests/api/base.ts +2 -1
  37. package/src/tests/api/restore.ts +1 -0
  38. package/src/tests/jestEnv.ts +2 -1
  39. package/src/tests/jestSetup.ts +2 -5
  40. package/src/tests/logging.ts +34 -0
  41. package/src/tests/structures/index.ts +0 -2
  42. package/src/utilities/email.ts +3 -3
  43. package/src/tests/structures/users.ts +0 -37
@@ -26,6 +26,8 @@ function parseIntSafe(number: any) {
26
26
  }
27
27
  }
28
28
 
29
+ const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
30
+
29
31
  const environment = {
30
32
  // auth
31
33
  MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
@@ -49,7 +51,7 @@ const environment = {
49
51
  CLUSTER_PORT: process.env.CLUSTER_PORT,
50
52
  // flags
51
53
  NODE_ENV: process.env.NODE_ENV,
52
- SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
54
+ SELF_HOSTED: selfHosted,
53
55
  LOG_LEVEL: process.env.LOG_LEVEL,
54
56
  MULTI_TENANCY: process.env.MULTI_TENANCY,
55
57
  DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
@@ -65,6 +67,18 @@ const environment = {
65
67
  CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
66
68
  SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
67
69
  ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY,
70
+ /**
71
+ * Mock the email service in use - links to ethereal hosted emails are logged instead.
72
+ */
73
+ ENABLE_EMAIL_TEST_MODE: process.env.ENABLE_EMAIL_TEST_MODE,
74
+ /**
75
+ * Enable to allow an admin user to login using a password.
76
+ * This can be useful to prevent lockout when configuring SSO.
77
+ * However, this should be turned OFF by default for security purposes.
78
+ */
79
+ ENABLE_SSO_MAINTENANCE_MODE: selfHosted
80
+ ? process.env.ENABLE_SSO_MAINTENANCE_MODE
81
+ : false,
68
82
  _set(key: any, value: any) {
69
83
  process.env[key] = value
70
84
  // @ts-ignore
package/src/index.ts CHANGED
@@ -25,6 +25,12 @@ const koaSession = require("koa-session")
25
25
  const logger = require("koa-pino-logger")
26
26
  import destroyable from "server-destroy"
27
27
 
28
+ if (env.ENABLE_SSO_MAINTENANCE_MODE) {
29
+ console.warn(
30
+ "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
31
+ )
32
+ }
33
+
28
34
  // this will setup http and https proxies form env variables
29
35
  bootstrap()
30
36
 
@@ -24,7 +24,7 @@ describe("tenancy middleware", () => {
24
24
  })
25
25
 
26
26
  it("should get tenant id from header", async () => {
27
- const tenantId = structures.uuid()
27
+ const tenantId = structures.tenant.id()
28
28
  const headers = {
29
29
  [constants.Header.TENANT_ID]: tenantId,
30
30
  }
@@ -35,7 +35,7 @@ describe("tenancy middleware", () => {
35
35
  })
36
36
 
37
37
  it("should get tenant id from query param", async () => {
38
- const tenantId = structures.uuid()
38
+ const tenantId = structures.tenant.id()
39
39
  const res = await config.request.get(
40
40
  `/api/global/configs/checklist?tenantId=${tenantId}`
41
41
  )
@@ -43,7 +43,7 @@ describe("tenancy middleware", () => {
43
43
  })
44
44
 
45
45
  it("should get tenant id from subdomain", async () => {
46
- const tenantId = structures.uuid()
46
+ const tenantId = structures.tenant.id()
47
47
  const headers = {
48
48
  host: `${tenantId}.localhost:10000`,
49
49
  }
@@ -67,7 +67,7 @@ describe("tenancy middleware", () => {
67
67
  it("should throw when no tenant id is found", async () => {
68
68
  const res = await config.request.get(`/api/global/configs/checklist`)
69
69
  expect(res.status).toBe(403)
70
- expect(res.text).toBe("Tenant id not set")
70
+ expect(res.body).toEqual({ message: "Tenant id not set", status: 403 })
71
71
  expect(res.headers[constants.Header.TENANT_ID]).toBe(undefined)
72
72
  })
73
73
  })
@@ -1,5 +1,6 @@
1
1
  import { User } from "@budibase/types"
2
- import sdk from "../../sdk"
2
+ import * as usersSdk from "../../sdk/users"
3
+ import { platform } from "@budibase/backend-core"
3
4
 
4
5
  /**
5
6
  * Date:
@@ -9,11 +10,11 @@ import sdk from "../../sdk"
9
10
  * Re-sync the global-db users to the global-info db users
10
11
  */
11
12
  export const run = async (globalDb: any) => {
12
- const users = (await sdk.users.allUsers()) as User[]
13
+ const users = (await usersSdk.allUsers()) as User[]
13
14
  const promises = []
14
15
  for (let user of users) {
15
16
  promises.push(
16
- sdk.users.addTenant(user.tenantId, user._id as string, user.email)
17
+ platform.users.addUser(user.tenantId, user._id as string, user.email)
17
18
  )
18
19
  }
19
20
  await Promise.all(promises)
@@ -1 +1,2 @@
1
- export * from "./accounts"
1
+ export * as metadata from "./metadata"
2
+ export { accounts as api } from "@budibase/backend-core"
@@ -2,7 +2,6 @@ import { AccountMetadata } from "@budibase/types"
2
2
  import {
3
3
  db,
4
4
  StaticDatabases,
5
- HTTPError,
6
5
  DocumentType,
7
6
  SEPARATOR,
8
7
  } from "@budibase/backend-core"
@@ -0,0 +1,86 @@
1
+ import {
2
+ auth as authCore,
3
+ tenancy,
4
+ utils as coreUtils,
5
+ sessions,
6
+ events,
7
+ HTTPError,
8
+ } from "@budibase/backend-core"
9
+ import { PlatformLogoutOpts, User } from "@budibase/types"
10
+ import jwt from "jsonwebtoken"
11
+ import env from "../../environment"
12
+ import * as userSdk from "../users"
13
+ import * as emails from "../../utilities/email"
14
+ import * as redis from "../../utilities/redis"
15
+ import { EmailTemplatePurpose } from "../../constants"
16
+
17
+ // LOGIN / LOGOUT
18
+
19
+ export async function loginUser(user: User) {
20
+ const sessionId = coreUtils.newid()
21
+ const tenantId = tenancy.getTenantId()
22
+ await sessions.createASession(user._id!, { sessionId, tenantId })
23
+ const token = jwt.sign(
24
+ {
25
+ userId: user._id,
26
+ sessionId,
27
+ tenantId,
28
+ },
29
+ env.JWT_SECRET!
30
+ )
31
+ return token
32
+ }
33
+
34
+ export async function logout(opts: PlatformLogoutOpts) {
35
+ // TODO: This should be moved out of core and into worker only
36
+ // account-portal can call worker endpoint
37
+ return authCore.platformLogout(opts)
38
+ }
39
+
40
+ // PASSWORD MANAGEMENT
41
+
42
+ /**
43
+ * Reset the user password, used as part of a forgotten password flow.
44
+ */
45
+ export const reset = async (email: string) => {
46
+ const configured = await emails.isEmailConfigured()
47
+ if (!configured) {
48
+ throw new HTTPError(
49
+ "Please contact your platform administrator, SMTP is not configured.",
50
+ 400
51
+ )
52
+ }
53
+
54
+ const user = await userSdk.core.getGlobalUserByEmail(email)
55
+ // exit if user doesn't exist
56
+ if (!user) {
57
+ return
58
+ }
59
+
60
+ // exit if user has sso
61
+ if (await userSdk.isPreventSSOPasswords(user)) {
62
+ throw new HTTPError("SSO user cannot reset password", 400)
63
+ }
64
+
65
+ // send password reset
66
+ await emails.sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, {
67
+ user,
68
+ subject: "{{ company }} platform password reset",
69
+ })
70
+ await events.user.passwordResetRequested(user)
71
+ }
72
+
73
+ /**
74
+ * Perform the user password update if the provided reset code is valid.
75
+ */
76
+ export const resetUpdate = async (resetCode: string, password: string) => {
77
+ const { userId } = await redis.checkResetPasswordCode(resetCode)
78
+
79
+ let user = await userSdk.getUser(userId)
80
+ user.password = password
81
+ user = await userSdk.save(user)
82
+
83
+ // remove password from the user before sending events
84
+ delete user.password
85
+ await events.user.passwordReset(user)
86
+ }
@@ -0,0 +1 @@
1
+ export * from "./auth"
@@ -0,0 +1 @@
1
+ export * from "./tenants"
@@ -0,0 +1,76 @@
1
+ import { App } from "@budibase/types"
2
+ import { tenancy, db as dbCore, platform } from "@budibase/backend-core"
3
+ import { quotas } from "@budibase/pro"
4
+
5
+ export async function deleteTenant(tenantId: string) {
6
+ await quotas.bustCache()
7
+ await platform.tenants.removeTenant(tenantId)
8
+ await removeGlobalDB(tenantId)
9
+ await removeTenantUsers(tenantId)
10
+ await removeTenantApps(tenantId)
11
+ }
12
+
13
+ async function removeGlobalDB(tenantId: string) {
14
+ try {
15
+ const db = tenancy.getTenantDB(tenantId)
16
+ await db.destroy()
17
+ } catch (err) {
18
+ console.error(`Error removing tenant ${tenantId} users from info db`, err)
19
+ throw err
20
+ }
21
+ }
22
+
23
+ async function removeTenantApps(tenantId: string) {
24
+ try {
25
+ const apps = (await dbCore.getAllApps({ all: true })) as App[]
26
+ const destroyPromises = apps.map(app => {
27
+ const db = dbCore.getDB(app.appId)
28
+ return db.destroy()
29
+ })
30
+ await Promise.allSettled(destroyPromises)
31
+ } catch (err) {
32
+ console.error(`Error removing tenant ${tenantId} apps`, err)
33
+ throw err
34
+ }
35
+ }
36
+
37
+ function getTenantUsers(tenantId: string) {
38
+ const db = tenancy.getTenantDB(tenantId)
39
+
40
+ return db.allDocs(
41
+ dbCore.getGlobalUserParams(null, {
42
+ include_docs: true,
43
+ })
44
+ )
45
+ }
46
+
47
+ async function removeTenantUsers(tenantId: string) {
48
+ try {
49
+ const allUsers = await getTenantUsers(tenantId)
50
+ const allEmails = allUsers.rows.map((row: any) => row.doc.email)
51
+
52
+ // get the id and email doc ids
53
+ let keys = allUsers.rows.map((row: any) => row.id)
54
+ keys = keys.concat(allEmails)
55
+
56
+ const platformDb = platform.getPlatformDB()
57
+
58
+ // retrieve the docs
59
+ const userDocs = await platformDb.allDocs({
60
+ keys,
61
+ include_docs: true,
62
+ })
63
+
64
+ // delete the docs
65
+ const toDelete = userDocs.rows.map((row: any) => {
66
+ return {
67
+ ...row.doc,
68
+ _deleted: true,
69
+ }
70
+ })
71
+ await platformDb.bulkDocs(toDelete)
72
+ } catch (err) {
73
+ console.error(`Error removing tenant ${tenantId} users from info db`, err)
74
+ throw err
75
+ }
76
+ }
@@ -84,6 +84,10 @@ export const handleSaveEvents = async (
84
84
  ) {
85
85
  await events.user.passwordForceReset(user)
86
86
  }
87
+
88
+ if (user.password !== existingUser.password) {
89
+ await events.user.passwordUpdated(user)
90
+ }
87
91
  } else {
88
92
  await events.user.created(user)
89
93
  }
@@ -1 +1,2 @@
1
1
  export * from "./users"
2
+ export { users as core } from "@budibase/backend-core"
@@ -0,0 +1,52 @@
1
+ import { structures } from "../../../tests"
2
+ import * as users from "../users"
3
+ import env from "../../../environment"
4
+ import { mocks } from "@budibase/backend-core/tests"
5
+ import { CloudAccount } from "@budibase/types"
6
+
7
+ describe("users", () => {
8
+ describe("isPreventSSOPasswords", () => {
9
+ it("returns true for sso account user", async () => {
10
+ const user = structures.users.user()
11
+ mocks.accounts.getAccount.mockReturnValue(
12
+ Promise.resolve(structures.accounts.ssoAccount() as CloudAccount)
13
+ )
14
+ const result = await users.isPreventSSOPasswords(user)
15
+ expect(result).toBe(true)
16
+ })
17
+
18
+ it("returns true for sso user", async () => {
19
+ const user = structures.users.ssoUser()
20
+ const result = await users.isPreventSSOPasswords(user)
21
+ expect(result).toBe(true)
22
+ })
23
+
24
+ describe("sso maintenance mode", () => {
25
+ beforeEach(() => {
26
+ env._set("ENABLE_SSO_MAINTENANCE_MODE", true)
27
+ })
28
+
29
+ afterEach(() => {
30
+ env._set("ENABLE_SSO_MAINTENANCE_MODE", false)
31
+ })
32
+
33
+ describe("non-admin user", () => {
34
+ it("returns true", async () => {
35
+ const user = structures.users.ssoUser()
36
+ const result = await users.isPreventSSOPasswords(user)
37
+ expect(result).toBe(true)
38
+ })
39
+ })
40
+
41
+ describe("admin user", () => {
42
+ it("returns false", async () => {
43
+ const user = structures.users.ssoUser({
44
+ user: structures.users.adminUser(),
45
+ })
46
+ const result = await users.isPreventSSOPasswords(user)
47
+ expect(result).toBe(false)
48
+ })
49
+ })
50
+ })
51
+ })
52
+ })
@@ -6,12 +6,11 @@ import {
6
6
  cache,
7
7
  constants,
8
8
  db as dbUtils,
9
- deprovisioning,
10
9
  events,
11
10
  HTTPError,
12
- migrations,
13
11
  sessions,
14
12
  tenancy,
13
+ platform,
15
14
  users as usersCore,
16
15
  utils,
17
16
  ViewName,
@@ -21,21 +20,22 @@ import {
21
20
  AllDocsResponse,
22
21
  BulkUserResponse,
23
22
  CloudAccount,
24
- CreateUserResponse,
25
23
  InviteUsersRequest,
26
24
  InviteUsersResponse,
27
- MigrationType,
25
+ isSSOAccount,
26
+ isSSOUser,
28
27
  PlatformUser,
29
28
  PlatformUserByEmail,
30
29
  RowResponse,
31
30
  SearchUsersRequest,
31
+ UpdateSelf,
32
32
  User,
33
- ThirdPartyUser,
34
- isUser,
33
+ SaveUserOpts,
35
34
  } from "@budibase/types"
36
35
  import { sendEmail } from "../../utilities/email"
37
36
  import { EmailTemplatePurpose } from "../../constants"
38
37
  import { groups as groupsSdk } from "@budibase/pro"
38
+ import * as accountSdk from "../accounts"
39
39
 
40
40
  const PAGE_LIMIT = 8
41
41
 
@@ -94,26 +94,23 @@ export const paginatedUsers = async ({
94
94
  })
95
95
  }
96
96
 
97
+ export async function getUserByEmail(email: string) {
98
+ return usersCore.getGlobalUserByEmail(email)
99
+ }
100
+
97
101
  /**
98
102
  * Gets a user by ID from the global database, based on the current tenancy.
99
103
  */
100
104
  export const getUser = async (userId: string) => {
101
- const db = tenancy.getGlobalDB()
102
- let user = await db.get(userId)
105
+ const user = await usersCore.getById(userId)
103
106
  if (user) {
104
107
  delete user.password
105
108
  }
106
109
  return user
107
110
  }
108
111
 
109
- export interface SaveUserOpts {
110
- hashPassword?: boolean
111
- requirePassword?: boolean
112
- currentUserId?: string
113
- }
114
-
115
112
  const buildUser = async (
116
- user: User | ThirdPartyUser,
113
+ user: User,
117
114
  opts: SaveUserOpts = {
118
115
  hashPassword: true,
119
116
  requirePassword: true,
@@ -121,11 +118,13 @@ const buildUser = async (
121
118
  tenantId: string,
122
119
  dbUser?: any
123
120
  ): Promise<User> => {
124
- let fullUser = user as User
125
- let { password, _id } = fullUser
121
+ let { password, _id } = user
126
122
 
127
123
  let hashedPassword
128
124
  if (password) {
125
+ if (await isPreventSSOPasswords(user)) {
126
+ throw new HTTPError("SSO user cannot set password", 400)
127
+ }
129
128
  hashedPassword = opts.hashPassword ? await utils.hash(password) : password
130
129
  } else if (dbUser) {
131
130
  hashedPassword = dbUser.password
@@ -135,10 +134,10 @@ const buildUser = async (
135
134
 
136
135
  _id = _id || dbUtils.generateGlobalUserID()
137
136
 
138
- fullUser = {
137
+ const fullUser = {
139
138
  createdAt: Date.now(),
140
139
  ...dbUser,
141
- ...fullUser,
140
+ ...user,
142
141
  _id,
143
142
  password: hashedPassword,
144
143
  tenantId,
@@ -189,10 +188,36 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
189
188
  }
190
189
  }
191
190
 
191
+ export async function isPreventSSOPasswords(user: User) {
192
+ // when in maintenance mode we allow sso users with the admin role
193
+ // to perform any password action - this prevents lockout
194
+ if (env.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
195
+ return false
196
+ }
197
+
198
+ // Check local sso
199
+ if (isSSOUser(user)) {
200
+ return true
201
+ }
202
+
203
+ // Check account sso
204
+ const account = await accountSdk.api.getAccount(user.email)
205
+ return !!(account && isSSOAccount(account))
206
+ }
207
+
208
+ export async function updateSelf(id: string, data: UpdateSelf) {
209
+ let user = await getUser(id)
210
+ user = {
211
+ ...user,
212
+ ...data,
213
+ }
214
+ return save(user)
215
+ }
216
+
192
217
  export const save = async (
193
- user: User | ThirdPartyUser,
218
+ user: User,
194
219
  opts: SaveUserOpts = {}
195
- ): Promise<CreateUserResponse> => {
220
+ ): Promise<User> => {
196
221
  // default booleans to true
197
222
  if (opts.hashPassword == null) {
198
223
  opts.hashPassword = true
@@ -264,7 +289,7 @@ export const save = async (
264
289
  builtUser._rev = response.rev
265
290
 
266
291
  await eventHelpers.handleSaveEvents(builtUser, dbUser)
267
- await addTenant(tenantId, _id, email)
292
+ await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
268
293
  await cache.user.invalidateUser(response.id)
269
294
 
270
295
  // let server know to sync user
@@ -272,11 +297,8 @@ export const save = async (
272
297
 
273
298
  await Promise.all(groupPromises)
274
299
 
275
- return {
276
- _id: response.id,
277
- _rev: response.rev,
278
- email,
279
- }
300
+ // finally returned the saved user from the db
301
+ return db.get(builtUser._id!)
280
302
  } catch (err: any) {
281
303
  if (err.status === 409) {
282
304
  throw "User exists already"
@@ -286,21 +308,6 @@ export const save = async (
286
308
  }
287
309
  }
288
310
 
289
- export const addTenant = async (
290
- tenantId: string,
291
- _id: string,
292
- email: string
293
- ) => {
294
- if (env.MULTI_TENANCY) {
295
- const afterCreateTenant = () =>
296
- migrations.backPopulateMigrations({
297
- type: MigrationType.GLOBAL,
298
- tenantId,
299
- })
300
- await tenancy.tryAddTenant(tenantId, _id, email, afterCreateTenant)
301
- }
302
- }
303
-
304
311
  const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
305
312
  const lcEmails = emails.map(email => email.toLowerCase())
306
313
  const params = {
@@ -432,7 +439,7 @@ export const bulkCreate = async (
432
439
  for (const user of usersToBulkSave) {
433
440
  // TODO: Refactor to bulk insert users into the info db
434
441
  // instead of relying on looping tenant creation
435
- await addTenant(tenantId, user._id, user.email)
442
+ await platform.users.addUser(tenantId, user._id, user.email)
436
443
  await eventHelpers.handleSaveEvents(user, undefined)
437
444
  await apps.syncUserInApps(user._id)
438
445
  }
@@ -550,7 +557,7 @@ export const bulkDelete = async (
550
557
 
551
558
  export const destroy = async (id: string, currentUser: any) => {
552
559
  const db = tenancy.getGlobalDB()
553
- const dbUser = await db.get(id)
560
+ const dbUser = (await db.get(id)) as User
554
561
  const userId = dbUser._id as string
555
562
 
556
563
  if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
@@ -566,7 +573,7 @@ export const destroy = async (id: string, currentUser: any) => {
566
573
  }
567
574
  }
568
575
 
569
- await deprovisioning.removeUserFromInfoDB(dbUser)
576
+ await platform.users.removeUser(dbUser)
570
577
 
571
578
  await db.remove(userId, dbUser._rev)
572
579
 
@@ -579,7 +586,7 @@ export const destroy = async (id: string, currentUser: any) => {
579
586
 
580
587
  const bulkDeleteProcessing = async (dbUser: User) => {
581
588
  const userId = dbUser._id as string
582
- await deprovisioning.removeUserFromInfoDB(dbUser)
589
+ await platform.users.removeUser(dbUser)
583
590
  await eventHelpers.handleDeleteEvents(dbUser)
584
591
  await cache.user.invalidateUser(userId)
585
592
  await sessions.invalidateSessions(userId, { reason: "bulk-deletion" })