391 lines
9.7 KiB
TypeScript
391 lines
9.7 KiB
TypeScript
import env from "../../environment"
|
|
import { quotas } from "@budibase/pro"
|
|
import * as apps from "../../utilities/appService"
|
|
import * as eventHelpers from "./events"
|
|
import {
|
|
events,
|
|
tenancy,
|
|
utils,
|
|
db as dbUtils,
|
|
constants,
|
|
cache,
|
|
users as usersCore,
|
|
deprovisioning,
|
|
sessions,
|
|
HTTPError,
|
|
accounts,
|
|
migrations,
|
|
} from "@budibase/backend-core"
|
|
import { MigrationType, UserGroup } from "@budibase/types"
|
|
import { build } from "joi"
|
|
|
|
const PAGE_LIMIT = 8
|
|
|
|
export const allUsers = async (newDb?: any) => {
|
|
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 paginatedUsers = async ({
|
|
page,
|
|
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
|
|
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,
|
|
getKey,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
bulkCreate: false,
|
|
},
|
|
tenantId: string,
|
|
dbUser?: any
|
|
) => {
|
|
let { password, _id } = user
|
|
|
|
// get the password, make sure one is defined
|
|
let hashedPassword
|
|
if (password) {
|
|
hashedPassword = opts.hashPassword ? await utils.hash(password) : password
|
|
} else if (opts.requirePassword) {
|
|
throw "Password must be specified."
|
|
}
|
|
|
|
_id = _id || dbUtils.generateGlobalUserID()
|
|
|
|
user = {
|
|
createdAt: Date.now(),
|
|
...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
|
|
}
|
|
|
|
export const save = async (
|
|
user: any,
|
|
opts: SaveUserOpts = {
|
|
hashPassword: true,
|
|
requirePassword: true,
|
|
bulkCreate: false,
|
|
}
|
|
) => {
|
|
const tenantId = tenancy.getTenantId()
|
|
const db = tenancy.getGlobalDB()
|
|
let { email, _id } = user
|
|
// make sure another user isn't using the same email
|
|
let dbUser: any
|
|
if (opts.bulkCreate) {
|
|
dbUser = null
|
|
} else if (email) {
|
|
// 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,
|
|
},
|
|
tenantId,
|
|
dbUser
|
|
)
|
|
|
|
try {
|
|
const putOpts = {
|
|
password: builtUser.password,
|
|
...user,
|
|
}
|
|
if (opts.bulkCreate) {
|
|
return putOpts
|
|
}
|
|
// save the user to db
|
|
let response
|
|
const putUserFn = () => {
|
|
return db.put(builtUser)
|
|
}
|
|
if (eventHelpers.isAddingBuilder(builtUser, dbUser)) {
|
|
response = await quotas.addDeveloper(putUserFn)
|
|
} else {
|
|
response = await putUserFn()
|
|
}
|
|
builtUser._rev = response.rev
|
|
|
|
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
|
await addTenant(tenantId, _id, email)
|
|
await cache.user.invalidateUser(response.id)
|
|
// let server know to sync user
|
|
await apps.syncUserInApps(builtUser._id)
|
|
|
|
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: any[], groups: any) => {
|
|
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)
|
|
await quotas.addDevelopers(() => db.bulkDocs(usersToBulkSave), builderCount)
|
|
|
|
// Post processing of bulk added users, i.e events and cache operations
|
|
for (const user of usersToBulkSave) {
|
|
delete user.password
|
|
await eventHelpers.handleSaveEvents(user, null)
|
|
await apps.syncUserInApps(user._id)
|
|
}
|
|
|
|
return usersToBulkSave
|
|
}
|
|
|
|
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,
|
|
}))
|
|
)
|
|
|
|
//Deletion post processing
|
|
for (let user of usersToDelete) {
|
|
await bulkDeleteProcessing(user)
|
|
}
|
|
|
|
await quotas.removeDevelopers(builderCount)
|
|
|
|
return { groupsToModify, usersResponse: response }
|
|
}
|
|
|
|
export const destroy = async (id: string, currentUser: any) => {
|
|
const db = tenancy.getGlobalDB()
|
|
const dbUser = await db.get(id)
|
|
|
|
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)
|
|
await db.remove(dbUser._id, dbUser._rev)
|
|
await eventHelpers.handleDeleteEvents(dbUser)
|
|
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)
|
|
}
|
|
|
|
const bulkDeleteProcessing = async (dbUser: any) => {
|
|
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)
|
|
}
|