1
0
Fork 0
mirror of synced 2024-07-02 21:10:43 +12:00
budibase/packages/worker/src/sdk/users/users.ts

414 lines
10 KiB
TypeScript
Raw Normal View History

2022-04-08 12:28:22 +12:00
import env from "../../environment"
import { quotas } from "@budibase/pro"
import * as apps from "../../utilities/appService"
import * as eventHelpers from "./events"
import {
2022-04-08 12:28:22 +12:00
tenancy,
utils,
db as dbUtils,
2022-04-08 12:28:22 +12:00
constants,
cache,
users as usersCore,
2022-04-08 12:28:22 +12:00
deprovisioning,
sessions,
HTTPError,
accounts,
migrations,
} from "@budibase/backend-core"
import { MigrationType, User } from "@budibase/types"
2022-07-27 21:20:39 +12:00
import { groups as groupUtils } from "@budibase/pro"
2022-05-25 07:01:13 +12:00
const PAGE_LIMIT = 8
export const allUsers = async () => {
const db = tenancy.getGlobalDB()
const response = await db.allDocs(
dbUtils.getGlobalUserParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
}
export const countUsersByApp = async (appId: string) => {
let response: any = await usersCore.searchGlobalUsersByApp(appId)
return {
userCount: response.length,
}
}
export const paginatedUsers = async ({
page,
2022-07-22 21:12:16 +12:00
email,
appId,
}: { page?: string; email?: string; appId?: string } = {}) => {
const db = tenancy.getGlobalDB()
// get one extra document, to have the next page
const opts: any = {
include_docs: true,
limit: PAGE_LIMIT + 1,
}
// add a startkey if the page was specified (anchor)
if (page) {
opts.startkey = page
}
// property specifies what to use for the page/anchor
2022-07-22 21:12:16 +12:00
let userList,
property = "_id",
getKey
if (appId) {
userList = await usersCore.searchGlobalUsersByApp(appId, opts)
getKey = (doc: any) => usersCore.getGlobalUserByAppPage(appId, doc)
} else if (email) {
userList = await usersCore.searchGlobalUsersByEmail(email, opts)
property = "email"
} else {
// no search, query allDocs
const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts))
userList = response.rows.map((row: any) => row.doc)
}
return dbUtils.pagination(userList, PAGE_LIMIT, {
paginate: true,
property,
2022-07-22 21:12:16 +12:00
getKey,
})
}
2022-04-08 12:28:22 +12:00
/**
* Gets a user by ID from the global database, based on the current tenancy.
*/
export const getUser = async (userId: string) => {
const db = tenancy.getGlobalDB()
let user
try {
user = await db.get(userId)
} catch (err: any) {
// no user found, just return nothing
if (err.status === 404) {
return {}
}
throw err
}
if (user) {
delete user.password
}
return user
}
interface SaveUserOpts {
hashPassword?: boolean
requirePassword?: boolean
bulkCreate?: boolean
}
export const buildUser = async (
user: any,
opts: SaveUserOpts = {
hashPassword: true,
requirePassword: true,
2022-07-26 05:57:10 +12:00
bulkCreate: false,
},
tenantId: string,
dbUser?: any
) => {
let { password, _id } = user
let hashedPassword
if (password) {
hashedPassword = opts.hashPassword ? await utils.hash(password) : password
} else if (dbUser) {
hashedPassword = dbUser.password
} else if (opts.requirePassword) {
throw "Password must be specified."
}
_id = _id || dbUtils.generateGlobalUserID()
2022-07-26 04:18:42 +12:00
user = {
createdAt: Date.now(),
2022-07-26 04:18:42 +12:00
...dbUser,
...user,
_id,
password: hashedPassword,
tenantId,
}
// make sure the roles object is always present
if (!user.roles) {
user.roles = {}
}
// add the active status to a user if its not provided
if (user.status == null) {
user.status = constants.UserStatus.ACTIVE
}
return user
}
2022-04-08 12:28:22 +12:00
export const save = async (
user: any,
opts: SaveUserOpts = {
hashPassword: true,
requirePassword: true,
bulkCreate: false,
}
2022-04-08 12:28:22 +12:00
) => {
const tenantId = tenancy.getTenantId()
const db = tenancy.getGlobalDB()
let { email, _id } = user
2022-04-08 12:28:22 +12:00
// make sure another user isn't using the same email
let dbUser: any
if (opts.bulkCreate) {
dbUser = null
} else if (email) {
2022-04-08 12:28:22 +12:00
// check budibase users inside the tenant
dbUser = await usersCore.getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
throw `Email address ${email} already in use.`
}
// check budibase users in other tenants
if (env.MULTI_TENANCY) {
const tenantUser = await tenancy.getTenantUser(email)
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
// check root account users in account portal
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(email)
if (account && account.verified && account.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
} else if (_id) {
dbUser = await db.get(_id)
}
let builtUser = await buildUser(
user,
{
hashPassword: true,
requirePassword: user.requirePassword,
},
2022-04-08 12:28:22 +12:00
tenantId,
dbUser
)
2022-04-08 12:28:22 +12:00
try {
const putOpts = {
password: builtUser.password,
...user,
}
if (opts.bulkCreate) {
return putOpts
}
2022-04-08 12:28:22 +12:00
// save the user to db
let response
const putUserFn = () => {
return db.put(builtUser)
2022-04-08 12:28:22 +12:00
}
2022-07-27 21:20:39 +12:00
if (eventHelpers.isAddingBuilder(builtUser, dbUser)) {
2022-04-08 12:28:22 +12:00
response = await quotas.addDeveloper(putUserFn)
} else {
response = await putUserFn()
}
builtUser._rev = response.rev
await eventHelpers.handleSaveEvents(builtUser, dbUser)
await addTenant(tenantId, _id, email)
2022-04-08 12:28:22 +12:00
await cache.user.invalidateUser(response.id)
// let server know to sync user
await apps.syncUserInApps(builtUser._id)
2022-04-08 12:28:22 +12:00
return {
_id: response.id,
_rev: response.rev,
email,
}
} catch (err: any) {
if (err.status === 409) {
throw "User exists already"
} else {
throw err
}
}
}
export const addTenant = async (
tenantId: string,
_id: string,
email: string
) => {
if (env.MULTI_TENANCY) {
const afterCreateTenant = () =>
migrations.backPopulateMigrations({
type: MigrationType.GLOBAL,
tenantId,
})
await tenancy.tryAddTenant(tenantId, _id, email, afterCreateTenant)
}
}
export const bulkCreate = async (
newUsersRequested: User[],
groups: string[]
) => {
const db = tenancy.getGlobalDB()
const tenantId = tenancy.getTenantId()
let usersToSave: any[] = []
let newUsers: any[] = []
const allUsers = await db.allDocs(
dbUtils.getGlobalUserParams(null, {
include_docs: true,
})
)
let mapped = allUsers.rows.map((row: any) => row.id)
const currentUserEmails = mapped.map((x: any) => x.email) || []
for (const newUser of newUsersRequested) {
if (
newUsers.find((x: any) => x.email === newUser.email) ||
currentUserEmails.includes(newUser.email)
) {
continue
}
newUser.userGroups = groups
newUsers.push(newUser)
}
// Figure out how many builders we are adding and create the promises
// array that will be called by bulkDocs
let builderCount = 0
newUsers.forEach((user: any) => {
if (eventHelpers.isAddingBuilder(user, null)) {
builderCount++
}
usersToSave.push(
buildUser(
user,
{
hashPassword: true,
requirePassword: user.requirePassword,
bulkCreate: false,
},
tenantId
)
)
})
const usersToBulkSave = await Promise.all(usersToSave)
2022-07-27 21:20:39 +12:00
await quotas.addDevelopers(() => db.bulkDocs(usersToBulkSave), builderCount)
// Post processing of bulk added users, i.e events and cache operations
for (const user of usersToBulkSave) {
2022-07-27 07:20:12 +12:00
await eventHelpers.handleSaveEvents(user, null)
await apps.syncUserInApps(user._id)
}
2022-07-27 21:20:39 +12:00
return usersToBulkSave.map(user => {
return {
_id: user._id,
email: user.email,
}
})
}
export const bulkDelete = async (userIds: any) => {
const db = tenancy.getGlobalDB()
let groupsToModify: any = {}
let builderCount = 0
// Get users and delete
let usersToDelete = (
await db.allDocs({
include_docs: true,
keys: userIds,
})
).rows.map((user: any) => {
// if we find a user that has an associated group, add it to
// an array so we can easily use allDocs on them later.
// This prevents us having to re-loop over all the users
if (user.doc.userGroups) {
for (let groupId of user.doc.userGroups) {
if (!Object.keys(groupsToModify).includes(groupId)) {
groupsToModify[groupId] = [user.id]
} else {
groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
}
}
}
// Also figure out how many builders are being deleted
if (eventHelpers.isAddingBuilder(user.doc, null)) {
builderCount++
}
return user.doc
})
const response = await db.bulkDocs(
usersToDelete.map((user: any) => ({
...user,
_deleted: true,
}))
)
2022-07-27 21:20:39 +12:00
await groupUtils.bulkDeleteGroupUsers(groupsToModify)
//Deletion post processing
for (let user of usersToDelete) {
await bulkDeleteProcessing(user)
}
await quotas.removeDevelopers(builderCount)
2022-07-27 21:20:39 +12:00
return response
}
2022-04-08 12:28:22 +12:00
export const destroy = async (id: string, currentUser: any) => {
const db = tenancy.getGlobalDB()
const dbUser = await db.get(id)
let groups = dbUser.userGroups
2022-04-08 12:28:22 +12:00
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
// root account holder can't be deleted from inside budibase
const email = dbUser.email
const account = await accounts.getAccount(email)
if (account) {
if (email === currentUser.email) {
throw new HTTPError('Please visit "Account" to delete this user', 400)
} else {
throw new HTTPError("Account holder cannot be deleted", 400)
}
}
}
await deprovisioning.removeUserFromInfoDB(dbUser)
2022-04-08 12:28:22 +12:00
await db.remove(dbUser._id, dbUser._rev)
if (groups) {
await groupUtils.deleteGroupUsers(groups, dbUser)
}
2022-05-24 09:14:44 +12:00
await eventHelpers.handleDeleteEvents(dbUser)
2022-04-08 12:28:22 +12:00
await quotas.removeUser(dbUser)
await cache.user.invalidateUser(dbUser._id)
await sessions.invalidateSessions(dbUser._id)
// let server know to sync user
await apps.syncUserInApps(dbUser._id)
}
2022-07-27 21:25:06 +12:00
const bulkDeleteProcessing = async (dbUser: User) => {
await deprovisioning.removeUserFromInfoDB(dbUser)
await eventHelpers.handleDeleteEvents(dbUser)
await cache.user.invalidateUser(dbUser._id)
await sessions.invalidateSessions(dbUser._id)
// let server know to sync user
await apps.syncUserInApps(dbUser._id)
}