forked from tsai/budibase
64 changed files with 1292 additions and 1299 deletions
@ -1,22 +1,13 @@ |
|||||
|
import { Screen } from "./utils/Screen" |
||||
|
|
||||
export default { |
export default { |
||||
name: `Create from scratch`, |
name: `Create from scratch`, |
||||
create: () => createScreen(), |
create: () => createScreen(), |
||||
} |
} |
||||
|
|
||||
const createScreen = () => ({ |
const createScreen = () => { |
||||
props: { |
return new Screen() |
||||
_id: "", |
.mainType("div") |
||||
_component: "@budibase/standard-components/container", |
.component("@budibase/standard-components/container") |
||||
_styles: { |
.json() |
||||
normal: {}, |
} |
||||
hover: {}, |
|
||||
active: {}, |
|
||||
selected: {}, |
|
||||
}, |
|
||||
type: "div", |
|
||||
_children: [], |
|
||||
_instanceName: "", |
|
||||
}, |
|
||||
route: "", |
|
||||
name: "screen-id", |
|
||||
}) |
|
||||
|
|||||
@ -1,22 +1,13 @@ |
|||||
|
import { Screen } from "./utils/Screen" |
||||
|
|
||||
export default { |
export default { |
||||
name: `New Row (Empty)`, |
name: `New Row (Empty)`, |
||||
create: () => createScreen(), |
create: () => createScreen(), |
||||
} |
} |
||||
|
|
||||
const createScreen = () => ({ |
const createScreen = () => { |
||||
props: { |
return new Screen() |
||||
_id: "", |
.component("@budibase/standard-components/newrow") |
||||
_component: "@budibase/standard-components/newrow", |
.table("") |
||||
_styles: { |
.json() |
||||
normal: {}, |
} |
||||
hover: {}, |
|
||||
active: {}, |
|
||||
selected: {}, |
|
||||
}, |
|
||||
_children: [], |
|
||||
_instanceName: "", |
|
||||
table: "", |
|
||||
}, |
|
||||
route: "", |
|
||||
name: "screen-id", |
|
||||
}) |
|
||||
|
|||||
@ -1,22 +1,13 @@ |
|||||
|
import { Screen } from "./utils/Screen" |
||||
|
|
||||
export default { |
export default { |
||||
name: `Row Detail (Empty)`, |
name: `Row Detail (Empty)`, |
||||
create: () => createScreen(), |
create: () => createScreen(), |
||||
} |
} |
||||
|
|
||||
const createScreen = () => ({ |
const createScreen = () => { |
||||
props: { |
return new Screen() |
||||
_id: "", |
.component("@budibase/standard-components/rowdetail") |
||||
_component: "@budibase/standard-components/rowdetail", |
.table("") |
||||
_styles: { |
.json() |
||||
normal: {}, |
} |
||||
hover: {}, |
|
||||
active: {}, |
|
||||
selected: {}, |
|
||||
}, |
|
||||
_children: [], |
|
||||
_instanceName: "", |
|
||||
table: "", |
|
||||
}, |
|
||||
route: "", |
|
||||
name: "screen-id", |
|
||||
}) |
|
||||
|
|||||
@ -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 Router = require("@koa/router") |
||||
const controller = require("../controllers/accesslevel") |
const controller = require("../controllers/accesslevel") |
||||
|
const authorized = require("../../middleware/authorized") |
||||
|
const { BUILDER } = require("../../utilities/security/permissions") |
||||
|
|
||||
const router = Router() |
const router = Router() |
||||
|
|
||||
router |
router |
||||
.post("/api/accesslevels", controller.create) |
.post("/api/accesslevels", authorized(BUILDER), controller.save) |
||||
.put("/api/accesslevels", controller.update) |
.get("/api/accesslevels", authorized(BUILDER), controller.fetch) |
||||
.get("/api/accesslevels", controller.fetch) |
.get("/api/accesslevels/:levelId", authorized(BUILDER), controller.find) |
||||
.get("/api/accesslevels/:levelId", controller.find) |
.delete( |
||||
.delete("/api/accesslevels/:levelId/:rev", controller.destroy) |
"/api/accesslevels/:levelId/:rev", |
||||
.patch("/api/accesslevels/:levelId", controller.patch) |
authorized(BUILDER), |
||||
|
controller.destroy |
||||
|
) |
||||
|
|
||||
module.exports = router |
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