mirror of https://github.com/Budibase/budibase.git
41 changed files with 861 additions and 340 deletions
@ -0,0 +1,11 @@ |
|||
const { BudibaseError } = require("./base") |
|||
|
|||
class GenericError extends BudibaseError { |
|||
constructor(message, code, type) { |
|||
super(message, code, type ? type : "generic") |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
GenericError, |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
const { GenericError } = require("./generic") |
|||
|
|||
class HTTPError extends GenericError { |
|||
constructor(message, httpStatus, code, type) { |
|||
super(message, code ? code : "http", type) |
|||
this.status = httpStatus |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
HTTPError, |
|||
} |
|||
@ -1,19 +1,29 @@ |
|||
const events = require("../events") |
|||
const { Events } = require("../constants") |
|||
|
|||
exports.created = () => { |
|||
/* eslint-disable */ |
|||
|
|||
exports.created = role => { |
|||
const properties = {} |
|||
events.processEvent(Events.ROLE_CREATED, properties) |
|||
} |
|||
|
|||
// TODO
|
|||
exports.deleted = () => { |
|||
exports.updated = role => { |
|||
const properties = {} |
|||
events.processEvent(Events.ROLE_UPDATED, properties) |
|||
} |
|||
|
|||
exports.deleted = role => { |
|||
const properties = {} |
|||
events.processEvent(Events.ROLE_DELETED, properties) |
|||
} |
|||
|
|||
// TODO
|
|||
exports.assigned = () => { |
|||
exports.assigned = (user, role) => { |
|||
const properties = {} |
|||
events.processEvent(Events.ROLE_ASSIGNED, properties) |
|||
} |
|||
|
|||
exports.unassigned = (user, role) => { |
|||
const properties = {} |
|||
events.processEvent(Events.ROLE_UNASSIGNED, properties) |
|||
} |
|||
|
|||
@ -1,85 +1,87 @@ |
|||
const events = require("../events") |
|||
const { Events } = require("../constants") |
|||
|
|||
// TODO
|
|||
exports.created = () => { |
|||
/* eslint-disable */ |
|||
|
|||
exports.created = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_CREATED, properties) |
|||
} |
|||
|
|||
// TODO
|
|||
exports.updated = () => { |
|||
exports.updated = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_UPDATED, properties) |
|||
} |
|||
|
|||
exports.deleted = () => { |
|||
exports.deleted = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_DELETED, properties) |
|||
} |
|||
|
|||
// TODO
|
|||
exports.passwordForceReset = () => { |
|||
exports.passwordForceReset = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_PASSWORD_FORCE_RESET, properties) |
|||
} |
|||
|
|||
// PERMISSIONS
|
|||
|
|||
// TODO
|
|||
exports.permissionAdminAssigned = () => { |
|||
exports.permissionAdminAssigned = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_PERMISSION_ADMIN_ASSIGNED, properties) |
|||
} |
|||
|
|||
// TODO
|
|||
exports.permissionAdminRemoved = () => { |
|||
exports.permissionAdminRemoved = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_PERMISSION_ADMIN_REMOVED, properties) |
|||
} |
|||
|
|||
// TODO
|
|||
exports.permissionBuilderAssigned = () => { |
|||
exports.permissionBuilderAssigned = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_PERMISSION_BUILDER_ASSIGNED, properties) |
|||
} |
|||
|
|||
// TODO
|
|||
exports.permissionBuilderRemoved = () => { |
|||
exports.permissionBuilderRemoved = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_PERMISSION_BUILDER_REMOVED, properties) |
|||
} |
|||
|
|||
// INVITE
|
|||
|
|||
exports.invited = () => { |
|||
// TODO
|
|||
exports.invited = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_INVITED, properties) |
|||
} |
|||
|
|||
exports.inviteAccepted = () => { |
|||
// TODO
|
|||
exports.inviteAccepted = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_INVITED_ACCEPTED, properties) |
|||
} |
|||
|
|||
// SELF
|
|||
|
|||
exports.selfUpdated = () => { |
|||
// TODO
|
|||
exports.selfUpdated = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_SELF_UPDATED, properties) |
|||
} |
|||
|
|||
exports.selfPasswordUpdated = () => { |
|||
// TODO
|
|||
exports.selfPasswordUpdated = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_SELF_PASSWORD_UPDATED, properties) |
|||
} |
|||
|
|||
exports.passwordResetRequested = () => { |
|||
// TODO
|
|||
exports.passwordResetRequested = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_PASSWORD_RESET_REQUESTED, properties) |
|||
} |
|||
|
|||
exports.passwordReset = () => { |
|||
// TODO
|
|||
exports.passwordReset = user => { |
|||
const properties = {} |
|||
events.processEvent(Events.USER_PASSWORD_RESET, properties) |
|||
} |
|||
|
|||
@ -0,0 +1,19 @@ |
|||
const { ViewNames } = require("./db/utils") |
|||
const { queryGlobalView } = require("./db/views") |
|||
|
|||
/** |
|||
* Given an email address this will use a view to search through |
|||
* all the users to find one with this email address. |
|||
* @param {string} email the email to lookup the user by. |
|||
* @return {Promise<object|null>} |
|||
*/ |
|||
exports.getGlobalUserByEmail = async email => { |
|||
if (email == null) { |
|||
throw "Must supply an email address to view" |
|||
} |
|||
|
|||
return queryGlobalView(ViewNames.USER_BY_EMAIL, { |
|||
key: email.toLowerCase(), |
|||
include_docs: true, |
|||
}) |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * as users from "./users" |
|||
@ -1,8 +1,8 @@ |
|||
const joiValidator = require("../../middleware/joi-validator") |
|||
const Joi = require("joi") |
|||
import joiValidator from "../../../middleware/joi-validator" |
|||
import Joi from "joi" |
|||
|
|||
exports.buildUserSaveValidation = (isSelf = false) => { |
|||
let schema = { |
|||
export const buildUserSaveValidation = (isSelf = false) => { |
|||
let schema: any = { |
|||
email: Joi.string().allow(null, ""), |
|||
password: Joi.string().allow(null, ""), |
|||
forceResetPassword: Joi.boolean().optional(), |
|||
@ -1,33 +0,0 @@ |
|||
const { getGlobalDB } = require("@budibase/backend-core/tenancy") |
|||
const { getGlobalUserParams } = require("@budibase/backend-core/db") |
|||
|
|||
/** |
|||
* Retrieves all users from the current tenancy. |
|||
*/ |
|||
exports.allUsers = async () => { |
|||
const db = getGlobalDB() |
|||
const response = await db.allDocs( |
|||
getGlobalUserParams(null, { |
|||
include_docs: true, |
|||
}) |
|||
) |
|||
return response.rows.map(row => row.doc) |
|||
} |
|||
|
|||
/** |
|||
* Gets a user by ID from the global database, based on the current tenancy. |
|||
*/ |
|||
exports.getUser = async userId => { |
|||
const db = getGlobalDB() |
|||
let user |
|||
try { |
|||
user = await db.get(userId) |
|||
} catch (err) { |
|||
// no user found, just return nothing
|
|||
user = {} |
|||
} |
|||
if (user) { |
|||
delete user.password |
|||
} |
|||
return user |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * as users from "./users" |
|||
@ -0,0 +1,136 @@ |
|||
const { events } = require("@budibase/backend-core") |
|||
|
|||
export const handleDeleteEvents = (user: any) => { |
|||
events.user.deleted(user) |
|||
|
|||
if (isBuilder(user)) { |
|||
events.user.permissionBuilderRemoved(user) |
|||
} |
|||
|
|||
if (isAdmin(user)) { |
|||
events.user.permissionAdminRemoved(user) |
|||
} |
|||
} |
|||
|
|||
const assignAppRoleEvents = (roles: any, existingRoles: any) => { |
|||
for (const [appId, role] of Object.entries(roles)) { |
|||
// app role in existing is not same as new
|
|||
if (!existingRoles || existingRoles[appId] !== role) { |
|||
events.role.assigned(role) |
|||
} |
|||
} |
|||
} |
|||
|
|||
const unassignAppRoleEvents = (roles: any, existingRoles: any) => { |
|||
if (!existingRoles) { |
|||
return |
|||
} |
|||
for (const [appId, role] of Object.entries(existingRoles)) { |
|||
// app role in new is not same as existing
|
|||
if (!roles || roles[appId] !== role) { |
|||
events.role.unassigned(role) |
|||
} |
|||
} |
|||
} |
|||
|
|||
const handleAppRoleEvents = (user: any, existingUser: any) => { |
|||
const roles = user.roles |
|||
const existingRoles = existingUser?.roles |
|||
|
|||
assignAppRoleEvents(roles, existingRoles) |
|||
unassignAppRoleEvents(roles, existingRoles) |
|||
} |
|||
|
|||
export const handleSaveEvents = (user: any, existingUser: any) => { |
|||
if (existingUser) { |
|||
events.user.updated(user) |
|||
|
|||
if (isRemovingBuilder(user, existingUser)) { |
|||
events.user.permissionBuilderRemoved(user) |
|||
} |
|||
|
|||
if (isRemovingAdmin(user, existingUser)) { |
|||
events.user.permissionAdminRemoved(user) |
|||
} |
|||
} else { |
|||
events.user.created(user) |
|||
} |
|||
|
|||
if (isAddingBuilder(user, existingUser)) { |
|||
events.user.permissionBuilderAssigned(user) |
|||
} |
|||
|
|||
if (isAddingAdmin(user, existingUser)) { |
|||
events.user.permissionAdminAssigned(user) |
|||
} |
|||
|
|||
handleAppRoleEvents(user, existingUser) |
|||
} |
|||
|
|||
const isBuilder = (user: any) => user.builder && user.builder.global |
|||
const isAdmin = (user: any) => user.admin && user.admin.global |
|||
|
|||
export const isAddingBuilder = (user: any, existingUser: any) => { |
|||
return isAddingPermission(user, existingUser, isBuilder) |
|||
} |
|||
|
|||
export const isRemovingBuilder = (user: any, existingUser: any) => { |
|||
return isRemovingPermission(user, existingUser, isBuilder) |
|||
} |
|||
|
|||
const isAddingAdmin = (user: any, existingUser: any) => { |
|||
return isAddingPermission(user, existingUser, isAdmin) |
|||
} |
|||
|
|||
const isRemovingAdmin = (user: any, existingUser: any) => { |
|||
return isRemovingPermission(user, existingUser, isAdmin) |
|||
} |
|||
|
|||
/** |
|||
* Check if a permission is being added to a new or existing user. |
|||
*/ |
|||
const isAddingPermission = ( |
|||
user: any, |
|||
existingUser: any, |
|||
hasPermission: any |
|||
) => { |
|||
// new user doesn't have the permission
|
|||
if (!hasPermission(user)) { |
|||
return false |
|||
} |
|||
|
|||
// existing user has the permission
|
|||
if (existingUser && hasPermission(existingUser)) { |
|||
return false |
|||
} |
|||
|
|||
// permission is being added
|
|||
return true |
|||
} |
|||
|
|||
/** |
|||
* Check if a permission is being removed from an existing user. |
|||
*/ |
|||
const isRemovingPermission = ( |
|||
user: any, |
|||
existingUser: any, |
|||
hasPermission: any |
|||
) => { |
|||
// new user has the permission
|
|||
if (hasPermission(user)) { |
|||
return false |
|||
} |
|||
|
|||
// no existing user or existing user doesn't have the permission
|
|||
if (!existingUser) { |
|||
return false |
|||
} |
|||
|
|||
// existing user doesn't have the permission
|
|||
if (!hasPermission(existingUser)) { |
|||
return false |
|||
} |
|||
|
|||
// permission is being removed
|
|||
return true |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from "./users" |
|||
@ -0,0 +1,180 @@ |
|||
import env from "../../environment" |
|||
import { quotas } from "@budibase/pro" |
|||
import * as apps from "../../utilities/appService" |
|||
const { events } = require("@budibase/backend-core") |
|||
import * as eventHelpers from "./events" |
|||
|
|||
const { |
|||
tenancy, |
|||
accounts, |
|||
utils, |
|||
db: dbUtils, |
|||
constants, |
|||
cache, |
|||
users: usersCore, |
|||
deprovisioning, |
|||
sessions, |
|||
HTTPError, |
|||
} = require("@budibase/backend-core") |
|||
|
|||
/** |
|||
* Retrieves all users from the current tenancy. |
|||
*/ |
|||
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) |
|||
} |
|||
|
|||
/** |
|||
* 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 |
|||
} |
|||
|
|||
export const save = async ( |
|||
user: any, |
|||
hashPassword = true, |
|||
requirePassword = true |
|||
) => { |
|||
const tenantId = tenancy.getTenantId() |
|||
|
|||
// specify the tenancy incase we're making a new admin user (public)
|
|||
const db = tenancy.getGlobalDB(tenantId) |
|||
let { email, password, _id } = user |
|||
// make sure another user isn't using the same email
|
|||
let dbUser |
|||
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) |
|||
} |
|||
|
|||
// get the password, make sure one is defined
|
|||
let hashedPassword |
|||
if (password) { |
|||
hashedPassword = hashPassword ? await utils.hash(password) : password |
|||
} else if (dbUser) { |
|||
hashedPassword = dbUser.password |
|||
} else if (requirePassword) { |
|||
throw "Password must be specified." |
|||
} |
|||
|
|||
if (!_id) { |
|||
_id = dbUtils.generateGlobalUserID(email) |
|||
} |
|||
|
|||
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 |
|||
} |
|||
try { |
|||
// save the user to db
|
|||
let response |
|||
const putUserFn = () => { |
|||
return db.put(user) |
|||
} |
|||
if (await eventHelpers.isAddingBuilder(user, dbUser)) { |
|||
response = await quotas.addDeveloper(putUserFn) |
|||
} else { |
|||
response = await putUserFn() |
|||
} |
|||
|
|||
eventHelpers.handleSaveEvents(user, dbUser) |
|||
|
|||
await tenancy.tryAddTenant(tenantId, _id, email) |
|||
await cache.user.invalidateUser(response.id) |
|||
// let server know to sync user
|
|||
await apps.syncUserInApps(user._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 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) |
|||
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) |
|||
} |
|||
@ -1,10 +1,12 @@ |
|||
const configs = require("./configs") |
|||
const users = require("./users") |
|||
|
|||
const TENANT_ID = "default" |
|||
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" |
|||
|
|||
module.exports = { |
|||
configs, |
|||
users, |
|||
TENANT_ID, |
|||
CSRF_TOKEN, |
|||
} |
|||
|
|||
@ -0,0 +1,28 @@ |
|||
export const email = "test@test.com" |
|||
|
|||
export const user = (userProps: any) => { |
|||
return { |
|||
email: "test@test.com", |
|||
password: "test", |
|||
roles: {}, |
|||
...userProps, |
|||
} |
|||
} |
|||
|
|||
export const adminUser = (userProps: any) => { |
|||
return { |
|||
...user(userProps), |
|||
admin: { |
|||
global: true, |
|||
}, |
|||
} |
|||
} |
|||
|
|||
export const builderUser = (userProps: any) => { |
|||
return { |
|||
...user(userProps), |
|||
builder: { |
|||
global: true, |
|||
}, |
|||
} |
|||
} |
|||
Loading…
Reference in new issue