mirror of https://github.com/Budibase/budibase.git
64 changed files with 1292 additions and 1299 deletions
@ -1,22 +1,13 @@ |
|||
import { Screen } from "./utils/Screen" |
|||
|
|||
export default { |
|||
name: `Create from scratch`, |
|||
create: () => createScreen(), |
|||
} |
|||
|
|||
const createScreen = () => ({ |
|||
props: { |
|||
_id: "", |
|||
_component: "@budibase/standard-components/container", |
|||
_styles: { |
|||
normal: {}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
type: "div", |
|||
_children: [], |
|||
_instanceName: "", |
|||
}, |
|||
route: "", |
|||
name: "screen-id", |
|||
}) |
|||
const createScreen = () => { |
|||
return new Screen() |
|||
.mainType("div") |
|||
.component("@budibase/standard-components/container") |
|||
.json() |
|||
} |
|||
|
|||
@ -1,22 +1,13 @@ |
|||
import { Screen } from "./utils/Screen" |
|||
|
|||
export default { |
|||
name: `New Row (Empty)`, |
|||
create: () => createScreen(), |
|||
} |
|||
|
|||
const createScreen = () => ({ |
|||
props: { |
|||
_id: "", |
|||
_component: "@budibase/standard-components/newrow", |
|||
_styles: { |
|||
normal: {}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_children: [], |
|||
_instanceName: "", |
|||
table: "", |
|||
}, |
|||
route: "", |
|||
name: "screen-id", |
|||
}) |
|||
const createScreen = () => { |
|||
return new Screen() |
|||
.component("@budibase/standard-components/newrow") |
|||
.table("") |
|||
.json() |
|||
} |
|||
|
|||
@ -1,22 +1,13 @@ |
|||
import { Screen } from "./utils/Screen" |
|||
|
|||
export default { |
|||
name: `Row Detail (Empty)`, |
|||
create: () => createScreen(), |
|||
} |
|||
|
|||
const createScreen = () => ({ |
|||
props: { |
|||
_id: "", |
|||
_component: "@budibase/standard-components/rowdetail", |
|||
_styles: { |
|||
normal: {}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_children: [], |
|||
_instanceName: "", |
|||
table: "", |
|||
}, |
|||
route: "", |
|||
name: "screen-id", |
|||
}) |
|||
const createScreen = () => { |
|||
return new Screen() |
|||
.component("@budibase/standard-components/rowdetail") |
|||
.table("") |
|||
.json() |
|||
} |
|||
|
|||
@ -0,0 +1,35 @@ |
|||
import { cloneDeep } from "lodash/fp" |
|||
|
|||
export class BaseStructure { |
|||
constructor(isScreen) { |
|||
this._isScreen = isScreen |
|||
this._children = [] |
|||
this._json = {} |
|||
} |
|||
|
|||
addChild(child) { |
|||
this._children.push(child) |
|||
return this |
|||
} |
|||
|
|||
customProps(props) { |
|||
for (let key of Object.keys(props)) { |
|||
this._json[key] = props[key] |
|||
} |
|||
return this |
|||
} |
|||
|
|||
json() { |
|||
const structure = cloneDeep(this._json) |
|||
if (this._children.length !== 0) { |
|||
for (let child of this._children) { |
|||
if (this._isScreen) { |
|||
structure.props._children.push(child.json()) |
|||
} else { |
|||
structure._children.push(child.json()) |
|||
} |
|||
} |
|||
} |
|||
return structure |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
import { cloneDeep } from "lodash/fp" |
|||
import { v4 } from "uuid" |
|||
import { BaseStructure } from "./BaseStructure" |
|||
|
|||
export class Component extends BaseStructure { |
|||
constructor(name) { |
|||
super(false) |
|||
this._children = [] |
|||
this._json = { |
|||
_id: v4(), |
|||
_component: name, |
|||
_styles: { |
|||
normal: {}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_code: "", |
|||
className: "", |
|||
onLoad: [], |
|||
type: "", |
|||
_instanceName: "", |
|||
_children: [], |
|||
} |
|||
} |
|||
|
|||
type(type) { |
|||
this._json.type = type |
|||
return this |
|||
} |
|||
|
|||
normalStyle(styling) { |
|||
this._json._styles.normal = styling |
|||
return this |
|||
} |
|||
|
|||
hoverStyle(styling) { |
|||
this._json._styles.hover = styling |
|||
return this |
|||
} |
|||
|
|||
text(text) { |
|||
this._json.text = text |
|||
return this |
|||
} |
|||
|
|||
// TODO: do we need this
|
|||
instanceName(name) { |
|||
this._json._instanceName = name |
|||
return this |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
import { BaseStructure } from "./BaseStructure" |
|||
|
|||
export class Screen extends BaseStructure { |
|||
constructor() { |
|||
super(true) |
|||
this._json = { |
|||
props: { |
|||
_id: "", |
|||
_component: "", |
|||
_styles: { |
|||
normal: {}, |
|||
hover: {}, |
|||
active: {}, |
|||
selected: {}, |
|||
}, |
|||
_children: [], |
|||
_instanceName: "", |
|||
}, |
|||
routing: { |
|||
route: "", |
|||
accessLevelId: "", |
|||
}, |
|||
name: "screen-id", |
|||
} |
|||
} |
|||
|
|||
component(name) { |
|||
this._json.props._component = name |
|||
return this |
|||
} |
|||
|
|||
table(tableName) { |
|||
this._json.props.table = tableName |
|||
return this |
|||
} |
|||
|
|||
mainType(type) { |
|||
this._json.type = type |
|||
return this |
|||
} |
|||
|
|||
route(route) { |
|||
this._json.routing.route = route |
|||
return this |
|||
} |
|||
|
|||
name(name) { |
|||
this._json.name = name |
|||
return this |
|||
} |
|||
|
|||
instanceName(name) { |
|||
this._json.props._instanceName = name |
|||
return this |
|||
} |
|||
} |
|||
@ -0,0 +1,145 @@ |
|||
import { Component } from "./Component" |
|||
import { rowListUrl } from "../rowListScreen" |
|||
|
|||
export function makeLinkComponent(tableName) { |
|||
return new Component("@budibase/standard-components/link") |
|||
.normalStyle({ |
|||
color: "#757575", |
|||
"text-transform": "capitalize", |
|||
}) |
|||
.hoverStyle({ |
|||
color: "#4285f4", |
|||
}) |
|||
.text(tableName) |
|||
.customProps({ |
|||
url: `/${tableName.toLowerCase()}`, |
|||
openInNewTab: false, |
|||
color: "", |
|||
hoverColor: "", |
|||
underline: false, |
|||
fontSize: "", |
|||
fontFamily: "initial", |
|||
}) |
|||
} |
|||
|
|||
export function makeMainContainer() { |
|||
return new Component("@budibase/standard-components/container") |
|||
.type("div") |
|||
.normalStyle({ |
|||
width: "700px", |
|||
padding: "0px", |
|||
background: "white", |
|||
"border-radius": "0.5rem", |
|||
"box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", |
|||
margin: "auto", |
|||
"margin-top": "20px", |
|||
"padding-top": "48px", |
|||
"padding-bottom": "48px", |
|||
"padding-right": "48px", |
|||
"padding-left": "48px", |
|||
"margin-bottom": "20px", |
|||
}) |
|||
.instanceName("Container") |
|||
} |
|||
|
|||
export function makeBreadcrumbContainer(tableName, text, capitalise = false) { |
|||
const link = makeLinkComponent(tableName).instanceName("Back Link") |
|||
|
|||
const arrowText = new Component("@budibase/standard-components/text") |
|||
.type("none") |
|||
.normalStyle({ |
|||
"margin-right": "4px", |
|||
"margin-left": "4px", |
|||
}) |
|||
.text(">") |
|||
.instanceName("Arrow") |
|||
|
|||
const textStyling = { |
|||
color: "#000000", |
|||
} |
|||
if (capitalise) { |
|||
textStyling["text-transform"] = "capitalize" |
|||
} |
|||
const identifierText = new Component("@budibase/standard-components/text") |
|||
.type("none") |
|||
.normalStyle(textStyling) |
|||
.text(text) |
|||
.instanceName("Identifier") |
|||
|
|||
return new Component("@budibase/standard-components/container") |
|||
.type("div") |
|||
.normalStyle({ |
|||
"font-size": "14px", |
|||
color: "#757575", |
|||
}) |
|||
.instanceName("Breadcrumbs") |
|||
.addChild(link) |
|||
.addChild(arrowText) |
|||
.addChild(identifierText) |
|||
} |
|||
|
|||
export function makeSaveButton(table) { |
|||
return new Component("@budibase/standard-components/button") |
|||
.normalStyle({ |
|||
background: "#000000", |
|||
"border-width": "0", |
|||
"border-style": "None", |
|||
color: "#fff", |
|||
"font-family": "Inter", |
|||
"font-weight": "500", |
|||
"font-size": "14px", |
|||
"margin-left": "16px", |
|||
}) |
|||
.hoverStyle({ |
|||
background: "#4285f4", |
|||
}) |
|||
.text("Save") |
|||
.customProps({ |
|||
className: "", |
|||
disabled: false, |
|||
onClick: [ |
|||
{ |
|||
parameters: { |
|||
contextPath: "data", |
|||
tableId: table._id, |
|||
}, |
|||
"##eventHandlerType": "Save Row", |
|||
}, |
|||
{ |
|||
parameters: { |
|||
url: rowListUrl(table), |
|||
}, |
|||
"##eventHandlerType": "Navigate To", |
|||
}, |
|||
], |
|||
}) |
|||
.instanceName("Save Button") |
|||
} |
|||
|
|||
export function makeTitleContainer(title) { |
|||
const heading = new Component("@budibase/standard-components/heading") |
|||
.normalStyle({ |
|||
margin: "0px", |
|||
"margin-bottom": "0px", |
|||
"margin-right": "0px", |
|||
"margin-top": "0px", |
|||
"margin-left": "0px", |
|||
flex: "1 1 auto", |
|||
}) |
|||
.type("h3") |
|||
.instanceName("Title") |
|||
.text(title) |
|||
|
|||
return new Component("@budibase/standard-components/container") |
|||
.type("div") |
|||
.normalStyle({ |
|||
display: "flex", |
|||
"flex-direction": "row", |
|||
"justify-content": "space-between", |
|||
"align-items": "center", |
|||
"margin-top": "32px", |
|||
"margin-bottom": "32px", |
|||
}) |
|||
.instanceName("Title Container") |
|||
.addChild(heading) |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
const { getRoutingInfo } = require("../../utilities/routing") |
|||
const { |
|||
getUserAccessLevelHierarchy, |
|||
BUILTIN_LEVEL_IDS, |
|||
} = require("../../utilities/security/accessLevels") |
|||
|
|||
const URL_SEPARATOR = "/" |
|||
|
|||
function Routing() { |
|||
this.json = {} |
|||
} |
|||
|
|||
Routing.prototype.getTopLevel = function(fullpath) { |
|||
if (fullpath.charAt(0) !== URL_SEPARATOR) { |
|||
fullpath = URL_SEPARATOR + fullpath |
|||
} |
|||
// replace the first value with the home route
|
|||
return URL_SEPARATOR + fullpath.split(URL_SEPARATOR)[1] |
|||
} |
|||
|
|||
Routing.prototype.getScreensProp = function(fullpath) { |
|||
const topLevel = this.getTopLevel(fullpath) |
|||
if (!this.json[topLevel]) { |
|||
this.json[topLevel] = { |
|||
subpaths: {}, |
|||
} |
|||
} |
|||
if (!this.json[topLevel].subpaths[fullpath]) { |
|||
this.json[topLevel].subpaths[fullpath] = { |
|||
screens: {}, |
|||
} |
|||
} |
|||
return this.json[topLevel].subpaths[fullpath].screens |
|||
} |
|||
|
|||
Routing.prototype.addScreenId = function(fullpath, accessLevel, screenId) { |
|||
this.getScreensProp(fullpath)[accessLevel] = screenId |
|||
} |
|||
|
|||
/** |
|||
* Gets the full routing structure by querying the routing view and processing the result into the tree. |
|||
* @param {string} appId The application to produce the routing structure for. |
|||
* @returns {Promise<object>} The routing structure, this is the full structure designed for use in the builder, |
|||
* if the client routing is required then the updateRoutingStructureForUserLevel should be used. |
|||
*/ |
|||
async function getRoutingStructure(appId) { |
|||
const screenRoutes = await getRoutingInfo(appId) |
|||
const routing = new Routing() |
|||
|
|||
for (let screenRoute of screenRoutes) { |
|||
let fullpath = screenRoute.routing.route |
|||
const accessLevel = screenRoute.routing.accessLevelId |
|||
routing.addScreenId(fullpath, accessLevel, screenRoute.id) |
|||
} |
|||
|
|||
return { routes: routing.json } |
|||
} |
|||
|
|||
exports.fetch = async ctx => { |
|||
ctx.body = await getRoutingStructure(ctx.appId) |
|||
} |
|||
|
|||
exports.clientFetch = async ctx => { |
|||
const routing = await getRoutingStructure(ctx.appId) |
|||
const accessLevelId = ctx.user.accessLevel._id |
|||
// builder is a special case, always return the full routing structure
|
|||
if (accessLevelId === BUILTIN_LEVEL_IDS.BUILDER) { |
|||
ctx.body = routing |
|||
return |
|||
} |
|||
const accessLevelIds = await getUserAccessLevelHierarchy( |
|||
ctx.appId, |
|||
accessLevelId |
|||
) |
|||
for (let topLevel of Object.values(routing.routes)) { |
|||
for (let subpathKey of Object.keys(topLevel.subpaths)) { |
|||
let found = false |
|||
const subpath = topLevel.subpaths[subpathKey] |
|||
const accessLevelOptions = Object.keys(subpath.screens) |
|||
if (accessLevelOptions.length === 1 && !accessLevelOptions[0]) { |
|||
subpath.screenId = subpath.screens[accessLevelOptions[0]] |
|||
subpath.accessLevelId = BUILTIN_LEVEL_IDS.BASIC |
|||
found = true |
|||
} else { |
|||
for (let levelId of accessLevelIds) { |
|||
if (accessLevelOptions.indexOf(levelId) !== -1) { |
|||
subpath.screenId = subpath.screens[levelId] |
|||
subpath.accessLevelId = levelId |
|||
found = true |
|||
break |
|||
} |
|||
} |
|||
} |
|||
delete subpath.screens |
|||
if (!found) { |
|||
delete topLevel.subpaths[subpathKey] |
|||
} |
|||
} |
|||
} |
|||
ctx.body = routing |
|||
} |
|||
@ -1,14 +1,18 @@ |
|||
const Router = require("@koa/router") |
|||
const controller = require("../controllers/accesslevel") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { BUILDER } = require("../../utilities/security/permissions") |
|||
|
|||
const router = Router() |
|||
|
|||
router |
|||
.post("/api/accesslevels", controller.create) |
|||
.put("/api/accesslevels", controller.update) |
|||
.get("/api/accesslevels", controller.fetch) |
|||
.get("/api/accesslevels/:levelId", controller.find) |
|||
.delete("/api/accesslevels/:levelId/:rev", controller.destroy) |
|||
.patch("/api/accesslevels/:levelId", controller.patch) |
|||
.post("/api/accesslevels", authorized(BUILDER), controller.save) |
|||
.get("/api/accesslevels", authorized(BUILDER), controller.fetch) |
|||
.get("/api/accesslevels/:levelId", authorized(BUILDER), controller.find) |
|||
.delete( |
|||
"/api/accesslevels/:levelId/:rev", |
|||
authorized(BUILDER), |
|||
controller.destroy |
|||
) |
|||
|
|||
module.exports = router |
|||
|
|||
@ -0,0 +1,13 @@ |
|||
const Router = require("@koa/router") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { BUILDER } = require("../../utilities/security/permissions") |
|||
const controller = require("../controllers/routing") |
|||
|
|||
const router = Router() |
|||
|
|||
// gets the full structure, not just the correct screen ID for your access level
|
|||
router |
|||
.get("/api/routing/client", controller.clientFetch) |
|||
.get("/api/routing", authorized(BUILDER), controller.fetch) |
|||
|
|||
module.exports = router |
|||
@ -1,36 +0,0 @@ |
|||
// Permissions
|
|||
module.exports.READ_TABLE = "read-table" |
|||
module.exports.WRITE_TABLE = "write-table" |
|||
module.exports.READ_VIEW = "read-view" |
|||
module.exports.EXECUTE_AUTOMATION = "execute-automation" |
|||
module.exports.EXECUTE_WEBHOOK = "execute-webhook" |
|||
module.exports.USER_MANAGEMENT = "user-management" |
|||
module.exports.BUILDER = "builder" |
|||
module.exports.LIST_USERS = "list-users" |
|||
// Access Level IDs
|
|||
module.exports.ADMIN_LEVEL_ID = "ADMIN" |
|||
module.exports.POWERUSER_LEVEL_ID = "POWER_USER" |
|||
module.exports.BUILDER_LEVEL_ID = "BUILDER" |
|||
module.exports.ANON_LEVEL_ID = "ANON" |
|||
module.exports.ACCESS_LEVELS = [ |
|||
module.exports.ADMIN_LEVEL_ID, |
|||
module.exports.POWERUSER_LEVEL_ID, |
|||
module.exports.BUILDER_LEVEL_ID, |
|||
module.exports.ANON_LEVEL_ID, |
|||
] |
|||
module.exports.PRETTY_ACCESS_LEVELS = { |
|||
[module.exports.ADMIN_LEVEL_ID]: "Admin", |
|||
[module.exports.POWERUSER_LEVEL_ID]: "Power user", |
|||
[module.exports.BUILDER_LEVEL_ID]: "Builder", |
|||
} |
|||
module.exports.adminPermissions = [ |
|||
{ |
|||
name: module.exports.USER_MANAGEMENT, |
|||
}, |
|||
] |
|||
|
|||
// to avoid circular dependencies this is included later, after exporting all enums
|
|||
const permissions = require("./permissions") |
|||
module.exports.generateAdminPermissions = permissions.generateAdminPermissions |
|||
module.exports.generatePowerUserPermissions = |
|||
permissions.generatePowerUserPermissions |
|||
@ -1,66 +0,0 @@ |
|||
const viewController = require("../api/controllers/view") |
|||
const tableController = require("../api/controllers/table") |
|||
const automationController = require("../api/controllers/automation") |
|||
const accessLevels = require("./accessLevels") |
|||
|
|||
// this has been broken out to reduce risk of circular dependency from utilities, no enums defined here
|
|||
const generateAdminPermissions = async appId => [ |
|||
...accessLevels.adminPermissions, |
|||
...(await generatePowerUserPermissions(appId)), |
|||
] |
|||
|
|||
const generatePowerUserPermissions = async appId => { |
|||
const fetchTablesCtx = { |
|||
user: { |
|||
appId, |
|||
}, |
|||
} |
|||
await tableController.fetch(fetchTablesCtx) |
|||
const tables = fetchTablesCtx.body |
|||
|
|||
const fetchViewsCtx = { |
|||
user: { |
|||
appId, |
|||
}, |
|||
} |
|||
await viewController.fetch(fetchViewsCtx) |
|||
const views = fetchViewsCtx.body |
|||
|
|||
const fetchAutomationsCtx = { |
|||
user: { |
|||
appId, |
|||
}, |
|||
} |
|||
await automationController.fetch(fetchAutomationsCtx) |
|||
const automations = fetchAutomationsCtx.body |
|||
|
|||
const readTablePermissions = tables.map(m => ({ |
|||
itemId: m._id, |
|||
name: accessLevels.READ_TABLE, |
|||
})) |
|||
|
|||
const writeTablePermissions = tables.map(m => ({ |
|||
itemId: m._id, |
|||
name: accessLevels.WRITE_TABLE, |
|||
})) |
|||
|
|||
const viewPermissions = views.map(v => ({ |
|||
itemId: v.name, |
|||
name: accessLevels.READ_VIEW, |
|||
})) |
|||
|
|||
const executeAutomationPermissions = automations.map(w => ({ |
|||
itemId: w._id, |
|||
name: accessLevels.EXECUTE_AUTOMATION, |
|||
})) |
|||
|
|||
return [ |
|||
...readTablePermissions, |
|||
...writeTablePermissions, |
|||
...viewPermissions, |
|||
...executeAutomationPermissions, |
|||
{ name: accessLevels.LIST_USERS }, |
|||
] |
|||
} |
|||
module.exports.generateAdminPermissions = generateAdminPermissions |
|||
module.exports.generatePowerUserPermissions = generatePowerUserPermissions |
|||
@ -0,0 +1,24 @@ |
|||
const CouchDB = require("../../db") |
|||
const { createRoutingView } = require("./routingUtils") |
|||
const { ViewNames, getQueryIndex, UNICODE_MAX } = require("../../db/utils") |
|||
|
|||
exports.getRoutingInfo = async appId => { |
|||
const db = new CouchDB(appId) |
|||
try { |
|||
const allRouting = await db.query(getQueryIndex(ViewNames.ROUTING), { |
|||
startKey: "", |
|||
endKey: UNICODE_MAX, |
|||
}) |
|||
return allRouting.rows.map(row => row.value) |
|||
} catch (err) { |
|||
// check if the view doesn't exist, it should for all new instances
|
|||
if (err != null && err.name === "not_found") { |
|||
await createRoutingView(appId) |
|||
return exports.getRoutingInfo(appId) |
|||
} else { |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
|
|||
exports.createRoutingView = createRoutingView |
|||
@ -0,0 +1,24 @@ |
|||
const CouchDB = require("../../db") |
|||
const { DocumentTypes, SEPARATOR, ViewNames } = require("../../db/utils") |
|||
const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR |
|||
|
|||
exports.createRoutingView = async appId => { |
|||
const db = new CouchDB(appId) |
|||
const designDoc = await db.get("_design/database") |
|||
const view = { |
|||
// if using variables in a map function need to inject them before use
|
|||
map: `function(doc) {
|
|||
if (doc._id.startsWith("${SCREEN_PREFIX}")) { |
|||
emit(doc._id, { |
|||
id: doc._id, |
|||
routing: doc.routing, |
|||
}) |
|||
} |
|||
}`,
|
|||
} |
|||
designDoc.views = { |
|||
...designDoc.views, |
|||
[ViewNames.ROUTING]: view, |
|||
} |
|||
await db.put(designDoc) |
|||
} |
|||
@ -0,0 +1,150 @@ |
|||
const CouchDB = require("../../db") |
|||
const { cloneDeep } = require("lodash/fp") |
|||
|
|||
const BUILTIN_IDS = { |
|||
ADMIN: "ADMIN", |
|||
POWER: "POWER_USER", |
|||
BASIC: "BASIC", |
|||
PUBLIC: "PUBLIC", |
|||
BUILDER: "BUILDER", |
|||
} |
|||
|
|||
function AccessLevel(id, name, inherits) { |
|||
this._id = id |
|||
this.name = name |
|||
if (inherits) { |
|||
this.inherits = inherits |
|||
} |
|||
} |
|||
|
|||
exports.BUILTIN_LEVELS = { |
|||
ADMIN: new AccessLevel(BUILTIN_IDS.ADMIN, "Admin", BUILTIN_IDS.POWER), |
|||
POWER: new AccessLevel(BUILTIN_IDS.POWER, "Power", BUILTIN_IDS.BASIC), |
|||
BASIC: new AccessLevel(BUILTIN_IDS.BASIC, "Basic", BUILTIN_IDS.PUBLIC), |
|||
ANON: new AccessLevel(BUILTIN_IDS.PUBLIC, "Public"), |
|||
BUILDER: new AccessLevel(BUILTIN_IDS.BUILDER, "Builder"), |
|||
} |
|||
|
|||
exports.BUILTIN_LEVEL_ID_ARRAY = Object.values(exports.BUILTIN_LEVELS).map( |
|||
level => level._id |
|||
) |
|||
|
|||
exports.BUILTIN_LEVEL_NAME_ARRAY = Object.values(exports.BUILTIN_LEVELS).map( |
|||
level => level.name |
|||
) |
|||
|
|||
function isBuiltin(accessLevel) { |
|||
return exports.BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevel) !== -1 |
|||
} |
|||
|
|||
/** |
|||
* Gets the access level object, this is mainly useful for two purposes, to check if the level exists and |
|||
* to check if the access level inherits any others. |
|||
* @param {string} appId The app in which to look for the access level. |
|||
* @param {string|null} accessLevelId The level ID to lookup. |
|||
* @returns {Promise<AccessLevel|object|null>} The access level object, which may contain an "inherits" property. |
|||
*/ |
|||
exports.getAccessLevel = async (appId, accessLevelId) => { |
|||
if (!accessLevelId) { |
|||
return null |
|||
} |
|||
let accessLevel |
|||
if (isBuiltin(accessLevelId)) { |
|||
accessLevel = cloneDeep( |
|||
Object.values(exports.BUILTIN_LEVELS).find( |
|||
level => level._id === accessLevelId |
|||
) |
|||
) |
|||
} else { |
|||
const db = new CouchDB(appId) |
|||
accessLevel = await db.get(accessLevelId) |
|||
} |
|||
return accessLevel |
|||
} |
|||
|
|||
/** |
|||
* Returns an ordered array of the user's inherited access level IDs, this can be used |
|||
* to determine if a user can access something that requires a specific access level. |
|||
* @param {string} appId The ID of the application from which access levels should be obtained. |
|||
* @param {string} userAccessLevelId The user's access level, this can be found in their access token. |
|||
* @returns {Promise<string[]>} returns an ordered array of the access levels, with the first being their |
|||
* highest level of access and the last being the lowest level. |
|||
*/ |
|||
exports.getUserAccessLevelHierarchy = async (appId, userAccessLevelId) => { |
|||
// special case, if they don't have a level then they are a public user
|
|||
if (!userAccessLevelId) { |
|||
return [BUILTIN_IDS.PUBLIC] |
|||
} |
|||
let accessLevelIds = [userAccessLevelId] |
|||
let userAccess = await exports.getAccessLevel(appId, userAccessLevelId) |
|||
// check if inherited makes it possible
|
|||
while ( |
|||
userAccess && |
|||
userAccess.inherits && |
|||
accessLevelIds.indexOf(userAccess.inherits) === -1 |
|||
) { |
|||
accessLevelIds.push(userAccess.inherits) |
|||
// go to get the inherited incase it inherits anything
|
|||
userAccess = await exports.getAccessLevel(appId, userAccess.inherits) |
|||
} |
|||
// add the user's actual level at the end (not at start as that stops iteration
|
|||
return accessLevelIds |
|||
} |
|||
|
|||
class AccessController { |
|||
constructor(appId) { |
|||
this.appId = appId |
|||
this.userHierarchies = {} |
|||
} |
|||
|
|||
async hasAccess(tryingAccessLevelId, userAccessLevelId) { |
|||
// special cases, the screen has no access level, the access levels are the same or the user
|
|||
// is currently in the builder
|
|||
if ( |
|||
tryingAccessLevelId == null || |
|||
tryingAccessLevelId === "" || |
|||
tryingAccessLevelId === userAccessLevelId || |
|||
userAccessLevelId === BUILTIN_IDS.BUILDER |
|||
) { |
|||
return true |
|||
} |
|||
let accessLevelIds = this.userHierarchies[userAccessLevelId] |
|||
if (!accessLevelIds) { |
|||
accessLevelIds = await exports.getUserAccessLevelHierarchy( |
|||
this.appId, |
|||
userAccessLevelId |
|||
) |
|||
this.userHierarchies[userAccessLevelId] = userAccessLevelId |
|||
} |
|||
|
|||
return accessLevelIds.indexOf(tryingAccessLevelId) !== -1 |
|||
} |
|||
|
|||
async checkScreensAccess(screens, userAccessLevelId) { |
|||
let accessibleScreens = [] |
|||
// don't want to handle this with Promise.all as this would mean all custom access levels would be
|
|||
// retrieved at same time, it is likely a custom levels will be re-used and therefore want
|
|||
// to work in sync for performance save
|
|||
for (let screen of screens) { |
|||
const accessible = await this.checkScreenAccess(screen, userAccessLevelId) |
|||
if (accessible) { |
|||
accessibleScreens.push(accessible) |
|||
} |
|||
} |
|||
return accessibleScreens |
|||
} |
|||
|
|||
async checkScreenAccess(screen, userAccessLevelId) { |
|||
const accessLevelId = |
|||
screen && screen.routing ? screen.routing.accessLevelId : null |
|||
if (await this.hasAccess(accessLevelId, userAccessLevelId)) { |
|||
return screen |
|||
} |
|||
return null |
|||
} |
|||
} |
|||
|
|||
exports.AccessController = AccessController |
|||
exports.BUILTIN_LEVEL_IDS = BUILTIN_IDS |
|||
exports.isBuiltin = isBuiltin |
|||
exports.AccessLevel = AccessLevel |
|||
@ -0,0 +1,113 @@ |
|||
const { flatten } = require("lodash") |
|||
|
|||
const PermissionLevels = { |
|||
READ: "read", |
|||
WRITE: "write", |
|||
EXECUTE: "execute", |
|||
ADMIN: "admin", |
|||
} |
|||
|
|||
const PermissionTypes = { |
|||
TABLE: "table", |
|||
USER: "user", |
|||
AUTOMATION: "automation", |
|||
WEBHOOK: "webhook", |
|||
BUILDER: "builder", |
|||
VIEW: "view", |
|||
} |
|||
|
|||
function Permission(type, level) { |
|||
this.level = level |
|||
this.type = type |
|||
} |
|||
|
|||
/** |
|||
* Given the specified permission level for the user return the levels they are allowed to carry out. |
|||
* @param {string} userPermLevel The permission level of the user. |
|||
* @return {string[]} All the permission levels this user is allowed to carry out. |
|||
*/ |
|||
function getAllowedLevels(userPermLevel) { |
|||
switch (userPermLevel) { |
|||
case PermissionLevels.READ: |
|||
return [PermissionLevels.READ] |
|||
case PermissionLevels.WRITE: |
|||
return [PermissionLevels.READ, PermissionLevels.WRITE] |
|||
case PermissionLevels.EXECUTE: |
|||
return [PermissionLevels.EXECUTE] |
|||
case PermissionLevels.ADMIN: |
|||
return [ |
|||
PermissionLevels.READ, |
|||
PermissionLevels.WRITE, |
|||
PermissionLevels.EXECUTE, |
|||
] |
|||
default: |
|||
return [] |
|||
} |
|||
} |
|||
|
|||
exports.BUILTIN_PERMISSION_NAMES = { |
|||
READ_ONLY: "read_only", |
|||
WRITE: "write", |
|||
ADMIN: "admin", |
|||
POWER: "power", |
|||
} |
|||
|
|||
exports.BUILTIN_PERMISSIONS = { |
|||
READ_ONLY: { |
|||
name: exports.BUILTIN_PERMISSION_NAMES.READ_ONLY, |
|||
permissions: [ |
|||
new Permission(PermissionTypes.TABLE, PermissionLevels.READ), |
|||
new Permission(PermissionTypes.VIEW, PermissionLevels.READ), |
|||
], |
|||
}, |
|||
WRITE: { |
|||
name: exports.BUILTIN_PERMISSION_NAMES.WRITE, |
|||
permissions: [ |
|||
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE), |
|||
new Permission(PermissionTypes.VIEW, PermissionLevels.READ), |
|||
], |
|||
}, |
|||
POWER: { |
|||
name: exports.BUILTIN_PERMISSION_NAMES.POWER, |
|||
permissions: [ |
|||
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE), |
|||
new Permission(PermissionTypes.USER, PermissionLevels.READ), |
|||
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), |
|||
new Permission(PermissionTypes.VIEW, PermissionLevels.READ), |
|||
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ), |
|||
], |
|||
}, |
|||
ADMIN: { |
|||
name: exports.BUILTIN_PERMISSION_NAMES.ADMIN, |
|||
permissions: [ |
|||
new Permission(PermissionTypes.TABLE, PermissionLevels.ADMIN), |
|||
new Permission(PermissionTypes.USER, PermissionLevels.ADMIN), |
|||
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.ADMIN), |
|||
new Permission(PermissionTypes.VIEW, PermissionLevels.ADMIN), |
|||
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.READ), |
|||
], |
|||
}, |
|||
} |
|||
|
|||
exports.doesHavePermission = (permType, permLevel, userPermissionNames) => { |
|||
const builtins = Object.values(exports.BUILTIN_PERMISSIONS) |
|||
let permissions = flatten( |
|||
builtins |
|||
.filter(builtin => userPermissionNames.indexOf(builtin.name) !== -1) |
|||
.map(builtin => builtin.permissions) |
|||
) |
|||
for (let permission of permissions) { |
|||
if ( |
|||
permission.type === permType && |
|||
getAllowedLevels(permission.level).indexOf(permLevel) !== -1 |
|||
) { |
|||
return true |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
|
|||
// utility as a lot of things need simply the builder permission
|
|||
exports.BUILDER = PermissionTypes.BUILDER |
|||
exports.PermissionTypes = PermissionTypes |
|||
exports.PermissionLevels = PermissionLevels |
|||
Loading…
Reference in new issue