354 lines
8.9 KiB
TypeScript
354 lines
8.9 KiB
TypeScript
import {
|
|
directCouchFind,
|
|
DocumentType,
|
|
generateAppUserID,
|
|
getGlobalUserParams,
|
|
getProdAppID,
|
|
getUsersByAppParams,
|
|
pagination,
|
|
queryGlobalView,
|
|
queryGlobalViewRaw,
|
|
SEPARATOR,
|
|
UNICODE_MAX,
|
|
ViewName,
|
|
} from "../db"
|
|
import {
|
|
BulkDocsResponse,
|
|
ContextUser,
|
|
CouchFindOptions,
|
|
DatabaseQueryOpts,
|
|
SearchUsersRequest,
|
|
User,
|
|
} from "@budibase/types"
|
|
import * as context from "../context"
|
|
import { getGlobalDB } from "../context"
|
|
import { isCreator } from "./utils"
|
|
import { UserDB } from "./db"
|
|
import { dataFilters } from "@budibase/shared-core"
|
|
|
|
type GetOpts = { cleanup?: boolean }
|
|
|
|
function removeUserPassword(users: User | User[]) {
|
|
if (Array.isArray(users)) {
|
|
return users.map(user => {
|
|
if (user) {
|
|
delete user.password
|
|
return user
|
|
}
|
|
})
|
|
} else if (users) {
|
|
delete users.password
|
|
return users
|
|
}
|
|
return users
|
|
}
|
|
|
|
export async function bulkGetGlobalUsersById(
|
|
userIds: string[],
|
|
opts?: GetOpts
|
|
) {
|
|
const db = getGlobalDB()
|
|
let users = (
|
|
await db.allDocs({
|
|
keys: userIds,
|
|
include_docs: true,
|
|
})
|
|
).rows.map(row => row.doc) as User[]
|
|
if (opts?.cleanup) {
|
|
users = removeUserPassword(users) as User[]
|
|
}
|
|
return users
|
|
}
|
|
|
|
export async function getAllUserIds() {
|
|
const db = getGlobalDB()
|
|
const startKey = `${DocumentType.USER}${SEPARATOR}`
|
|
const response = await db.allDocs({
|
|
startkey: startKey,
|
|
endkey: `${startKey}${UNICODE_MAX}`,
|
|
})
|
|
return response.rows.map(row => row.id)
|
|
}
|
|
|
|
export async function getAllUsers(): Promise<User[]> {
|
|
const db = getGlobalDB()
|
|
const startKey = `${DocumentType.USER}${SEPARATOR}`
|
|
const response = await db.allDocs({
|
|
startkey: startKey,
|
|
endkey: `${startKey}${UNICODE_MAX}`,
|
|
include_docs: true,
|
|
})
|
|
return response.rows.map(row => row.doc) as User[]
|
|
}
|
|
|
|
export async function bulkUpdateGlobalUsers(users: User[]) {
|
|
const db = getGlobalDB()
|
|
return (await db.bulkDocs(users)) as BulkDocsResponse
|
|
}
|
|
|
|
export async function getById(id: string, opts?: GetOpts): Promise<User> {
|
|
const db = context.getGlobalDB()
|
|
let user = await db.get<User>(id)
|
|
if (opts?.cleanup) {
|
|
user = removeUserPassword(user) as User
|
|
}
|
|
return user
|
|
}
|
|
|
|
/**
|
|
* Given an email address this will use a view to search through
|
|
* all the users to find one with this email address.
|
|
*/
|
|
export async function getGlobalUserByEmail(
|
|
email: String,
|
|
opts?: GetOpts
|
|
): Promise<User | undefined> {
|
|
if (email == null) {
|
|
throw "Must supply an email address to view"
|
|
}
|
|
|
|
const response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
|
key: email.toLowerCase(),
|
|
include_docs: true,
|
|
})
|
|
|
|
if (Array.isArray(response)) {
|
|
// shouldn't be able to happen, but need to handle just in case
|
|
throw new Error(`Multiple users found with email address: ${email}`)
|
|
}
|
|
|
|
let user = response as User
|
|
if (opts?.cleanup) {
|
|
user = removeUserPassword(user) as User
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
export async function doesUserExist(email: string) {
|
|
try {
|
|
const user = await getGlobalUserByEmail(email)
|
|
if (Array.isArray(user) || user != null) {
|
|
return true
|
|
}
|
|
} catch (err) {
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
export async function searchGlobalUsersByApp(
|
|
appId: any,
|
|
opts: DatabaseQueryOpts,
|
|
getOpts?: GetOpts
|
|
) {
|
|
if (typeof appId !== "string") {
|
|
throw new Error("Must provide a string based app ID")
|
|
}
|
|
const params = getUsersByAppParams(appId, {
|
|
include_docs: true,
|
|
})
|
|
params.startkey = opts && opts.startkey ? opts.startkey : params.startkey
|
|
let response = await queryGlobalView<User>(ViewName.USER_BY_APP, params)
|
|
|
|
if (!response) {
|
|
response = []
|
|
}
|
|
let users: User[] = Array.isArray(response) ? response : [response]
|
|
if (getOpts?.cleanup) {
|
|
users = removeUserPassword(users) as User[]
|
|
}
|
|
return users
|
|
}
|
|
|
|
/*
|
|
Return any user who potentially has access to the application
|
|
Admins, developers and app users with the explicitly role.
|
|
*/
|
|
export async function searchGlobalUsersByAppAccess(
|
|
appId: any,
|
|
opts?: { limit?: number }
|
|
) {
|
|
const roleSelector = `roles.${appId}`
|
|
|
|
let orQuery: any[] = [
|
|
{
|
|
"builder.global": true,
|
|
},
|
|
{
|
|
"admin.global": true,
|
|
},
|
|
]
|
|
|
|
if (appId) {
|
|
const roleCheck = {
|
|
[roleSelector]: {
|
|
$exists: true,
|
|
},
|
|
}
|
|
orQuery.push(roleCheck)
|
|
}
|
|
|
|
let searchOptions: CouchFindOptions = {
|
|
selector: {
|
|
$or: orQuery,
|
|
_id: {
|
|
$regex: "^us_",
|
|
},
|
|
},
|
|
limit: opts?.limit || 50,
|
|
}
|
|
|
|
const resp = await directCouchFind(context.getGlobalDBName(), searchOptions)
|
|
return resp.rows
|
|
}
|
|
|
|
export function getGlobalUserByAppPage(appId: string, user: User) {
|
|
if (!user) {
|
|
return
|
|
}
|
|
return generateAppUserID(getProdAppID(appId)!, user._id!)
|
|
}
|
|
|
|
/**
|
|
* Performs a starts with search on the global email view.
|
|
*/
|
|
export async function searchGlobalUsersByEmail(
|
|
email: string | unknown,
|
|
opts: any,
|
|
getOpts?: GetOpts
|
|
) {
|
|
if (typeof email !== "string") {
|
|
throw new Error("Must provide a string to search by")
|
|
}
|
|
const lcEmail = email.toLowerCase()
|
|
// handle if passing up startkey for pagination
|
|
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
|
|
let response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
|
...opts,
|
|
startkey,
|
|
endkey: `${lcEmail}${UNICODE_MAX}`,
|
|
})
|
|
if (!response) {
|
|
response = []
|
|
}
|
|
let users: User[] = Array.isArray(response) ? response : [response]
|
|
if (getOpts?.cleanup) {
|
|
users = removeUserPassword(users) as User[]
|
|
}
|
|
return users
|
|
}
|
|
|
|
const PAGE_LIMIT = 8
|
|
export async function paginatedUsers({
|
|
bookmark,
|
|
query,
|
|
appId,
|
|
limit,
|
|
}: SearchUsersRequest = {}) {
|
|
const db = getGlobalDB()
|
|
const pageSize = limit ?? PAGE_LIMIT
|
|
const pageLimit = pageSize + 1
|
|
// get one extra document, to have the next page
|
|
const opts: DatabaseQueryOpts = {
|
|
include_docs: true,
|
|
limit: pageLimit,
|
|
}
|
|
// add a startkey if the page was specified (anchor)
|
|
if (bookmark) {
|
|
opts.startkey = bookmark
|
|
}
|
|
// property specifies what to use for the page/anchor
|
|
let userList: User[],
|
|
property = "_id",
|
|
getKey
|
|
if (query?.equal?._id) {
|
|
userList = [await getById(query.equal._id)]
|
|
} else if (appId) {
|
|
userList = await searchGlobalUsersByApp(appId, opts)
|
|
getKey = (doc: any) => getGlobalUserByAppPage(appId, doc)
|
|
} else if (query?.string?.email) {
|
|
userList = await searchGlobalUsersByEmail(query?.string?.email, opts)
|
|
property = "email"
|
|
} else if (query?.oneOf?._id) {
|
|
userList = await bulkGetGlobalUsersById(query?.oneOf?._id, {
|
|
cleanup: true,
|
|
})
|
|
} else if (query) {
|
|
// TODO: this should use SQS search, but the logic is built in the 'server' package. Using the in-memory filtering to get this working meanwhile
|
|
const response = await db.allDocs<User>(
|
|
getGlobalUserParams(null, { ...opts, limit: undefined })
|
|
)
|
|
userList = response.rows.map(row => row.doc!)
|
|
userList = dataFilters.search(userList, { query, limit: opts.limit }).rows
|
|
} else {
|
|
// no search, query allDocs
|
|
const response = await db.allDocs<User>(getGlobalUserParams(null, opts))
|
|
userList = response.rows.map(row => row.doc!)
|
|
}
|
|
return pagination(userList, pageSize, {
|
|
paginate: true,
|
|
property,
|
|
getKey,
|
|
})
|
|
}
|
|
|
|
export async function getUserCount() {
|
|
const response = await queryGlobalViewRaw(ViewName.USER_BY_EMAIL, {
|
|
limit: 0, // to be as fast as possible - we just want the total rows count
|
|
include_docs: false,
|
|
})
|
|
return response.total_rows
|
|
}
|
|
|
|
export async function getCreatorCount() {
|
|
let creators = 0
|
|
async function iterate(startPage?: string) {
|
|
const page = await paginatedUsers({ bookmark: startPage })
|
|
const creatorsEval = await Promise.all(page.data.map(isCreator))
|
|
creators += creatorsEval.filter(creator => !!creator).length
|
|
if (page.hasNextPage) {
|
|
await iterate(page.nextPage)
|
|
}
|
|
}
|
|
await iterate()
|
|
return creators
|
|
}
|
|
|
|
// used to remove the builder/admin permissions, for processing the
|
|
// user as an app user (they may have some specific role/group
|
|
export function removePortalUserPermissions(user: User | ContextUser) {
|
|
delete user.admin
|
|
delete user.builder
|
|
return user
|
|
}
|
|
|
|
export function cleanseUserObject(user: User | ContextUser, base?: User) {
|
|
delete user.admin
|
|
delete user.builder
|
|
delete user.roles
|
|
if (base) {
|
|
user.admin = base.admin
|
|
user.builder = base.builder
|
|
user.roles = base.roles
|
|
}
|
|
return user
|
|
}
|
|
|
|
export async function addAppBuilder(user: User, appId: string) {
|
|
const prodAppId = getProdAppID(appId)
|
|
user.builder ??= {}
|
|
user.builder.creator = true
|
|
user.builder.apps ??= []
|
|
user.builder.apps.push(prodAppId)
|
|
await UserDB.save(user, { hashPassword: false })
|
|
}
|
|
|
|
export async function removeAppBuilder(user: User, appId: string) {
|
|
const prodAppId = getProdAppID(appId)
|
|
if (user.builder && user.builder.apps?.includes(prodAppId)) {
|
|
user.builder.apps = user.builder.apps.filter(id => id !== prodAppId)
|
|
}
|
|
await UserDB.save(user, { hashPassword: false })
|
|
}
|