diff --git a/packages/builder/package.json b/packages/builder/package.json index ff249c0e40..9dab2460fb 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -81,7 +81,8 @@ "shortid": "^2.2.15", "svelte-loading-spinners": "^0.1.1", "svelte-portal": "^0.1.0", - "yup": "^0.29.2" + "yup": "^0.29.2", + "uuid": "^8.3.1" }, "devDependencies": { "@babel/core": "^7.5.5", diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 31b8dcff22..2c1def6efb 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -50,6 +50,7 @@ export const getFrontendStore = () => { return state }) const screens = await api.get("/api/screens").then(r => r.json()) + const routing = await api.get("/api/routing").then(r => r.json()) const mainScreens = screens.filter(screen => screen._id.includes(pkg.pages.main._id) diff --git a/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js b/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js index a8ab27df3d..b25562758e 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/createFromScratchScreen.js @@ -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() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js b/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js index e58319688b..a2f2f6df67 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/emptyNewRowScreen.js @@ -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() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js index a75de583cb..5dbdcf4e69 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/emptyRowDetailScreen.js @@ -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() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/index.js b/packages/builder/src/builderStore/store/screenTemplates/index.js index 5abe428966..ddf48cbe44 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/index.js +++ b/packages/builder/src/builderStore/store/screenTemplates/index.js @@ -24,7 +24,7 @@ const createTemplateOverride = (frontendState, create) => () => { } screen.props._id = uuid() screen.name = screen.props._id - screen.route = screen.route.toLowerCase() + screen.routing.route = screen.routing.route.toLowerCase() return screen } diff --git a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js index 50e90cddcf..1e699dad95 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js @@ -1,5 +1,12 @@ -import sanitizeUrl from "./sanitizeUrl" -import { rowListUrl } from "./rowListScreen" +import sanitizeUrl from "./utils/sanitizeUrl" +import { Component } from "./utils/Component" +import { Screen } from "./utils/Screen" +import { + makeBreadcrumbContainer, + makeMainContainer, + makeTitleContainer, + makeSaveButton, +} from "./utils/commonComponents" export default function(tables) { return tables.map(table => { @@ -14,242 +21,26 @@ export default function(tables) { export const newRowUrl = table => sanitizeUrl(`/${table.name}/new`) export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE" -const createScreen = table => ({ - props: { - _id: "c683c4ca8ffc849c6bdd3b7d637fbbf3c", - _component: "@budibase/standard-components/newrow", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - table: table._id, - _children: [ - { - _id: "ccad6cc135c7947a7ba9c631f655d6e0f", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - 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", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - onLoad: [], - type: "div", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Container", - _children: [ - { - _id: "c6e91622ba7984f468f70bf4bf5120246", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - "font-size": "14px", - color: "#757575", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - onLoad: [], - type: "div", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Breadcrumbs", - _children: [ - { - _id: "caa33353c252c4931b2a51b48a559a7fc", - _component: "@budibase/standard-components/link", - _styles: { - normal: { - color: "#757575", - "text-transform": "capitalize", - }, - hover: { - color: "#4285f4", - }, - active: {}, - selected: {}, - }, - _code: "", - url: `/${table.name.toLowerCase()}`, - openInNewTab: false, - text: table.name, - color: "", - hoverColor: "", - underline: false, - fontSize: "", - fontFamily: "initial", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Back Link", - _children: [], - }, - { - _id: "c6e218170201040e7a74e2c8304fe1860", - _component: "@budibase/standard-components/text", - _styles: { - normal: { - "margin-right": "4px", - "margin-left": "4px", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - text: ">", - type: "none", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Arrow", - _children: [], - }, - { - _id: "c799da1fa3a84442e947cc9199518f64c", - _component: "@budibase/standard-components/text", - _styles: { - normal: { - color: "#000000", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - text: "New", - type: "none", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Identifier", - _children: [], - }, - ], - }, - { - _id: "cbd1637cd1e274287a3c28ef0bf235d08", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - display: "flex", - "flex-direction": "row", - "justify-content": "space-between", - "align-items": "center", - "margin-top": "32px", - "margin-bottom": "32px", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - onLoad: [], - type: "div", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Title Container", - _children: [ - { - _id: "c98d3675d04114558bbf28661c5ccfb8e", - _component: "@budibase/standard-components/heading", - _styles: { - normal: { - margin: "0px", - "margin-bottom": "0px", - "margin-right": "0px", - "margin-top": "0px", - "margin-left": "0px", - flex: "1 1 auto", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - text: "New Row", - type: "h3", - _instanceName: "Title", - _children: [], - }, - { - _id: "cae402bd3c6a44618a8341bf7ab9ab086", - _component: "@budibase/standard-components/button", - _styles: { - normal: { - background: "#000000", - "border-width": "0", - "border-style": "None", - color: "#fff", - "font-family": "Inter", - "font-weight": "500", - "font-size": "14px", - "margin-left": "16px", - }, - hover: { - background: "#4285f4", - }, - active: {}, - selected: {}, - }, - _code: "", - text: "Save", - className: "", - disabled: false, - onClick: [ - { - parameters: { - contextPath: "data", - tableId: table._id, - }, - "##eventHandlerType": "Save Row", - }, - { - parameters: { - url: rowListUrl(table), - }, - "##eventHandlerType": "Navigate To", - }, - ], - _instanceName: "Save Button", - _children: [], - }, - ], - }, - { - _id: "c5e6c98d7363640f9ad3a7d19c8c10f67", - _component: "@budibase/standard-components/dataformwide", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Form", - _children: [], - }, - ], - }, - ], - _instanceName: `${table.name} - New`, - _code: "", - }, - route: newRowUrl(table), - name: "", -}) +function generateTitleContainer(table) { + return makeTitleContainer("New Row").addChild(makeSaveButton(table)) +} + +const createScreen = table => { + const dataform = new Component( + "@budibase/standard-components/dataformwide" + ).instanceName("Form") + + const container = makeMainContainer() + .addChild(makeBreadcrumbContainer(table.name, "New")) + .addChild(generateTitleContainer(table)) + .addChild(dataform) + + return new Screen() + .component("@budibase/standard-components/newrow") + .table(table._id) + .route(newRowUrl(table)) + .instanceName(`${table.name} - New`) + .name("") + .addChild(container) + .json() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js index a4f55f2fd1..526f457f3e 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js @@ -1,5 +1,13 @@ -import sanitizeUrl from "./sanitizeUrl" +import sanitizeUrl from "./utils/sanitizeUrl" import { rowListUrl } from "./rowListScreen" +import { Screen } from "./utils/Screen" +import { Component } from "./utils/Component" +import { + makeMainContainer, + makeBreadcrumbContainer, + makeTitleContainer, + makeSaveButton, +} from "./utils/commonComponents" export default function(tables) { return tables.map(table => { @@ -17,288 +25,78 @@ export default function(tables) { export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE" export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`) -const createScreen = (table, heading) => ({ - props: { - _id: "c683c4ca8ffc849c6bdd3b7d637fbbf3c", - _component: "@budibase/standard-components/rowdetail", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - table: table._id, - _children: [ - { - _id: "ccad6cc135c7947a7ba9c631f655d6e0f", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - 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", +function generateTitleContainer(table, title) { + // have to override style for this, its missing margin + const saveButton = makeSaveButton(table).normalStyle({ + background: "#000000", + "border-width": "0", + "border-style": "None", + color: "#fff", + "font-family": "Inter", + "font-weight": "500", + "font-size": "14px", + }) + + const deleteButton = new Component("@budibase/standard-components/button") + .normalStyle({ + background: "transparent", + "border-width": "0", + "border-style": "None", + color: "#9e9e9e", + "font-family": "Inter", + "font-weight": "500", + "font-size": "14px", + "margin-right": "8px", + "margin-left": "16px", + }) + .hoverStyle({ + background: "transparent", + color: "#4285f4", + }) + .text("Delete") + .customProps({ + className: "", + disabled: false, + onClick: [ + { + parameters: { + rowId: "{{ data._id }}", + revId: "{{ data._rev }}", + tableId: table._id, }, - hover: {}, - active: {}, - selected: {}, + "##eventHandlerType": "Delete Row", }, - _code: "", - className: "", - onLoad: [], - type: "div", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Container", - _children: [ - { - _id: "c6e91622ba7984f468f70bf4bf5120246", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - "font-size": "14px", - color: "#757575", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - onLoad: [], - type: "div", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Breadcrumbs", - _children: [ - { - _id: "caa33353c252c4931b2a51b48a559a7fc", - _component: "@budibase/standard-components/link", - _styles: { - normal: { - color: "#757575", - "text-transform": "capitalize", - }, - hover: { - color: "#4285f4", - }, - active: {}, - selected: {}, - }, - _code: "", - url: `/${table.name.toLowerCase()}`, - openInNewTab: false, - text: table.name, - color: "", - hoverColor: "", - underline: false, - fontSize: "", - fontFamily: "initial", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Back Link", - _children: [], - }, - { - _id: "c6e218170201040e7a74e2c8304fe1860", - _component: "@budibase/standard-components/text", - _styles: { - normal: { - "margin-right": "4px", - "margin-left": "4px", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - text: ">", - type: "none", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Arrow", - _children: [], - }, - { - _id: "c799da1fa3a84442e947cc9199518f64c", - _component: "@budibase/standard-components/text", - _styles: { - normal: { - color: "#000000", - "text-transform": "capitalize", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - text: heading || "Edit", - type: "none", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Identifier", - _children: [], - }, - ], + { + parameters: { + url: rowListUrl(table), }, - { - _id: "cbd1637cd1e274287a3c28ef0bf235d08", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - display: "flex", - "flex-direction": "row", - "justify-content": "space-between", - "align-items": "center", - "margin-top": "32px", - "margin-bottom": "32px", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - onLoad: [], - type: "div", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Title Container", - _children: [ - { - _id: "c98d3675d04114558bbf28661c5ccfb8e", - _component: "@budibase/standard-components/heading", - _styles: { - normal: { - margin: "0px", - "margin-bottom": "0px", - "margin-right": "0px", - "margin-top": "0px", - "margin-left": "0px", - flex: "1 1 auto", - "text-transform": "capitalize", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - text: heading || "Edit Row", - type: "h3", - _instanceName: "Title", - _children: [], - }, - { - _id: "c0a162cfb7d1c4bcfa8d24c290ccd1fd6", - _component: "@budibase/standard-components/button", - _styles: { - normal: { - background: "transparent", - "border-width": "0", - "border-style": "None", - color: "#9e9e9e", - "font-family": "Inter", - "font-weight": "500", - "font-size": "14px", - "margin-right": "8px", - "margin-left": "16px", - }, - hover: { - background: "transparent", - color: "#4285f4", - }, - active: {}, - selected: {}, - }, - _code: "", - text: "Delete", - className: "", - disabled: false, - onClick: [ - { - parameters: { - rowId: "{{ data._id }}", - revId: "{{ data._rev }}", - tableId: table._id, - }, - "##eventHandlerType": "Delete Row", - }, - { - parameters: { - url: rowListUrl(table), - }, - "##eventHandlerType": "Navigate To", - }, - ], - _instanceName: "Delete Button", - _children: [], - }, - { - _id: "cae402bd3c6a44618a8341bf7ab9ab086", - _component: "@budibase/standard-components/button", - _styles: { - normal: { - background: "#000000", - "border-width": "0", - "border-style": "None", - color: "#fff", - "font-family": "Inter", - "font-weight": "500", - "font-size": "14px", - }, - hover: { - background: "#4285f4", - }, - active: {}, - selected: {}, - }, - _code: "", - text: "Save", - className: "", - disabled: false, - onClick: [ - { - parameters: { - contextPath: "data", - tableId: table._id, - }, - "##eventHandlerType": "Save Row", - }, - { - parameters: { - url: rowListUrl(table), - }, - "##eventHandlerType": "Navigate To", - }, - ], - _instanceName: "Save Button", - _children: [], - }, - ], - }, - { - _id: "c5e6c98d7363640f9ad3a7d19c8c10f67", - _component: "@budibase/standard-components/dataformwide", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Form", - _children: [], - }, - ], - }, - ], - _instanceName: `${table.name} - Detail`, - _code: "", - }, - route: rowDetailUrl(table), - name: "", -}) + "##eventHandlerType": "Navigate To", + }, + ], + }) + .instanceName("Delete Button") + + return makeTitleContainer(title) + .addChild(deleteButton) + .addChild(saveButton) +} + +const createScreen = (table, heading) => { + const dataform = new Component( + "@budibase/standard-components/dataformwide" + ).instanceName("Form") + + const container = makeMainContainer() + .addChild(makeBreadcrumbContainer(table.name, heading || "Edit")) + .addChild(generateTitleContainer(table, heading || "Edit Row")) + .addChild(dataform) + + return new Screen() + .component("@budibase/standard-components/rowdetail") + .table(table._id) + .instanceName(`${table.name} - Detail`) + .route(rowDetailUrl(table)) + .name("") + .addChild(container) + .json() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js index 5c71a45f1f..54a066af9c 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js @@ -1,5 +1,7 @@ -import sanitizeUrl from "./sanitizeUrl" +import sanitizeUrl from "./utils/sanitizeUrl" import { newRowUrl } from "./newRowScreen" +import { Screen } from "./utils/Screen" +import { Component } from "./utils/Component" export default function(tables) { return tables.map(table => { @@ -14,159 +16,103 @@ export default function(tables) { export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE" export const rowListUrl = table => sanitizeUrl(`/${table.name}`) -const createScreen = table => ({ - props: { - _id: "c7365379815e4457dbe703a886c2da43b", - _component: "@budibase/standard-components/container", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - type: "div", - _children: [ - { - _id: "cf51241fc063d4d87be032dd509fe0244", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - background: "white", - "border-radius": "0.5rem", - "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", - margin: "auto", - "margin-top": "20px", - "border-width": "2px", - "border-color": "rgba(0, 0, 0, 0.1)", - "border-style": "None", - "padding-top": "48px", - "padding-bottom": "48px", - "padding-right": "48px", - "padding-left": "48px", - "margin-bottom": "20px", +function generateTitleContainer(table) { + const newButton = 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", + }) + .hoverStyle({ + background: "#4285f4", + }) + .text("Create New") + .customProps({ + className: "", + disabled: false, + onClick: [ + { + parameters: { + url: newRowUrl(table), }, - hover: {}, - active: {}, - selected: {}, + "##eventHandlerType": "Navigate To", }, - _code: "", - className: "", - onLoad: [], - type: "div", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Container", - _children: [ - { - _id: "c73294c301fd145aabe9bbbbd96a150ac", - _component: "@budibase/standard-components/container", - _styles: { - normal: { - display: "flex", - "flex-direction": "row", - "justify-content": "space-between", - "align-items": "center", - "margin-bottom": "32px", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - onLoad: [], - type: "div", - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Title Container", - _children: [ - { - _id: "c2b77901df95a4d1ca7204c58300bc94b", - _component: "@budibase/standard-components/heading", - _styles: { - normal: { - margin: "0px", - flex: "1 1 auto", - "text-transform": "capitalize", - }, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - className: "", - text: table.name, - type: "h3", - _instanceName: "Title", - _children: [], - }, - { - _id: "c12a82d77baf24ca9922ea0af7cd4f723", - _component: "@budibase/standard-components/button", - _styles: { - normal: { - background: "#000000", - "border-width": "0", - "border-style": "None", - color: "#fff", - "font-family": "Inter", - "font-weight": "500", - "font-size": "14px", - }, - hover: { - background: "#4285f4", - }, - active: {}, - selected: {}, - }, - _code: "", - text: "Create New", - className: "", - disabled: false, - onClick: [ - { - parameters: { - url: newRowUrl(table), - }, - "##eventHandlerType": "Navigate To", - }, - ], - _instanceName: "New Button", - _children: [], - }, - ], - }, - { - _id: "ca686a2ed89c943e6bafb63fa66a3ead3", - _component: "@budibase/standard-components/datagrid", - _styles: { - normal: {}, - hover: {}, - active: {}, - selected: {}, - }, - _code: "", - datasource: { - label: table.name, - name: `all_${table._id}`, - tableId: table._id, - type: "table", - }, - editable: false, - theme: "alpine", - height: "540", - pagination: true, - _instanceId: "inst_app_8fb_631af42f9dc94da2b5c48dc6c5124610", - _instanceName: "Grid", - _children: [], - detailUrl: `${table.name.toLowerCase()}/:id`, - }, - ], + ], + }) + .instanceName("New Button") + + const heading = new Component("@budibase/standard-components/heading") + .normalStyle({ + margin: "0px", + flex: "1 1 auto", + "text-transform": "capitalize", + }) + .type("h3") + .instanceName("Title") + .text(table.name) + + return new Component("@budibase/standard-components/container") + .type("div") + .normalStyle({ + display: "flex", + "flex-direction": "row", + "justify-content": "space-between", + "align-items": "center", + "margin-bottom": "32px", + }) + .instanceName("Title Container") + .addChild(heading) + .addChild(newButton) +} + +const createScreen = table => { + const datagrid = new Component("@budibase/standard-components/datagrid") + .customProps({ + datasource: { + label: table.name, + name: `all_${table._id}`, + tableId: table._id, + type: "table", }, - ], - _instanceName: `${table.name} - List`, - _code: "", - className: "", - onLoad: [], - }, - route: rowListUrl(table), - name: "", -}) + editable: false, + theme: "alpine", + height: "540", + pagination: true, + detailUrl: `${table.name.toLowerCase()}/:id`, + }) + .instanceName("Grid") + + const mainContainer = new Component("@budibase/standard-components/container") + .normalStyle({ + background: "white", + "border-radius": "0.5rem", + "box-shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)", + margin: "auto", + "margin-top": "20px", + "border-width": "2px", + "border-color": "rgba(0, 0, 0, 0.1)", + "border-style": "None", + "padding-top": "48px", + "padding-bottom": "48px", + "padding-right": "48px", + "padding-left": "48px", + "margin-bottom": "20px", + }) + .type("div") + .instanceName("Container") + .addChild(generateTitleContainer(table)) + .addChild(datagrid) + + return new Screen() + .component("@budibase/standard-components/container") + .mainType("div") + .route(rowListUrl(table)) + .instanceName(`${table.name} - List`) + .name("") + .addChild(mainContainer) + .json() +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/BaseStructure.js b/packages/builder/src/builderStore/store/screenTemplates/utils/BaseStructure.js new file mode 100644 index 0000000000..71daca9d1b --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/BaseStructure.js @@ -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 + } +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js new file mode 100644 index 0000000000..27b4af2d5b --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Component.js @@ -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 + } +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js new file mode 100644 index 0000000000..951e26aeb6 --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/Screen.js @@ -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 + } +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js new file mode 100644 index 0000000000..89e08cecdb --- /dev/null +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -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) +} diff --git a/packages/builder/src/builderStore/store/screenTemplates/sanitizeUrl.js b/packages/builder/src/builderStore/store/screenTemplates/utils/sanitizeUrl.js similarity index 100% rename from packages/builder/src/builderStore/store/screenTemplates/sanitizeUrl.js rename to packages/builder/src/builderStore/store/screenTemplates/utils/sanitizeUrl.js diff --git a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte index 39d5f92a92..f064ff923c 100644 --- a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte +++ b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte @@ -59,10 +59,13 @@ } // Create autolink to newly created list page - const listPage = screens.find(screen => + const listScreen = screens.find(screen => screen.props._instanceName.endsWith("List") ) - await store.actions.components.links.save(listPage.route, table.name) + await store.actions.components.links.save( + listScreen.routing.route, + table.name + ) // Navigate to new table $goto(`./table/${table._id}`) diff --git a/packages/builder/src/components/userInterface/DetailScreenSelect.svelte b/packages/builder/src/components/userInterface/DetailScreenSelect.svelte index e0b2813c0d..c119985196 100644 --- a/packages/builder/src/components/userInterface/DetailScreenSelect.svelte +++ b/packages/builder/src/components/userInterface/DetailScreenSelect.svelte @@ -17,11 +17,11 @@ .filter( screen => screen.props._component.endsWith("/rowdetail") || - screen.route.endsWith(":id") + screen.routing.route.endsWith(":id") ) .map(screen => ({ name: screen.props._instanceName, - url: screen.route, + url: screen.routing.route, sort: screen.props._component, })), ] diff --git a/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte b/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte index 948ab37efd..3475a14eed 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte @@ -25,7 +25,9 @@ + {/each} {:else} diff --git a/packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte b/packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte index 916f85dd0b..041237266e 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/actions/NavigateTo.svelte @@ -10,7 +10,7 @@ + {/each} diff --git a/packages/builder/src/components/userInterface/NewScreenModal.svelte b/packages/builder/src/components/userInterface/NewScreenModal.svelte index b194ab9839..ebee601838 100644 --- a/packages/builder/src/components/userInterface/NewScreenModal.svelte +++ b/packages/builder/src/components/userInterface/NewScreenModal.svelte @@ -49,8 +49,8 @@ baseComponent = draftScreen.props._component } - if (draftScreen.route) { - route = draftScreen.route + if (draftScreen.routing) { + route = draftScreen.routing.route } } @@ -69,7 +69,8 @@ draftScreen.props._instanceName = name draftScreen.props._component = baseComponent - draftScreen.route = route + // TODO: need to fix this up correctly + draftScreen.routing = { route, accessLevelId: "ADMIN" } await store.actions.screens.create(draftScreen) if (createLink) { @@ -88,7 +89,7 @@ const routeNameExists = route => { return $allScreens.some( - screen => screen.route.toLowerCase() === route.toLowerCase() + screen => screen.routing.route.toLowerCase() === route.toLowerCase() ) } diff --git a/packages/builder/src/components/userInterface/ScreenSelect.svelte b/packages/builder/src/components/userInterface/ScreenSelect.svelte index b38116e4a1..1c7827e2dd 100644 --- a/packages/builder/src/components/userInterface/ScreenSelect.svelte +++ b/packages/builder/src/components/userInterface/ScreenSelect.svelte @@ -21,7 +21,7 @@ .filter(screen => !screen.props._component.endsWith("/rowdetail")) .map(screen => ({ name: screen.props._instanceName, - url: screen.route, + url: screen.routing.route, sort: screen.props._component, })), ] @@ -54,7 +54,7 @@ if (idBinding) { urls.push({ name: detailScreen.props._instanceName, - url: detailScreen.route.replace( + url: detailScreen.routing.route.replace( ":id", `{{ ${idBinding.runtimeBinding} }}` ), diff --git a/packages/builder/yarn.lock b/packages/builder/yarn.lock index 289e9ec4b9..c1ede88884 100644 --- a/packages/builder/yarn.lock +++ b/packages/builder/yarn.lock @@ -1266,9 +1266,9 @@ integrity sha512-TSt8ROqK6wq+Hav7EhZL1I0GtsZhg28aJuuDSviBzG/NG9pC0eprf8roWjl59DKHOVWIUTPTeY+T+lipb9gf8w== "@testing-library/dom@^7.0.3": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.27.0.tgz#b4c7393f488db6de18c6cfa619390e7d6da57a3b" - integrity sha512-OH4lK9R0DdKdePG76cKQXORMpXLahqRLmyIau084TdN2sM1sfjxnGEiiTbJe/PDiam7sMOuf9uBXsTByH3pPhQ== + version "7.27.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.27.1.tgz#b760182513357e4448a8461f9565d733a88d71d0" + integrity sha512-AF56RoeUU8bO4DOvLyMI44H3O1LVKZQi2D/m5fNDr+iR4drfOFikTr26hT6IY7YG+l8g69FXsHERa+uThaYYQg== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" @@ -2103,9 +2103,9 @@ chalk@^4.0.0, chalk@^4.1.0: supports-color "^7.1.0" cheap-watch@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cheap-watch/-/cheap-watch-1.0.2.tgz#bfa648aea6bcd15e9fe4dce4ac760ab81cd0133c" - integrity sha512-jp82t+kZAW+ZVnuYuHZEGZqDaUg28uAyOhC915BcKBSYL55fpTyuJ56cYYXZG0JkCPQT80MjRD6q2KqebaPwCw== + version "1.0.3" + resolved "https://registry.yarnpkg.com/cheap-watch/-/cheap-watch-1.0.3.tgz#3c4265718bcf8f1ae08f5e450f9f4693432e028e" + integrity sha512-xC5CruMhLzjPwJ5ecUxGu1uGmwJQykUhqd2QrCrYbwvsFYdRyviu6jG9+pccwDXJR/OpmOTOJ9yLFunVgQu9wg== check-more-types@2.24.0, check-more-types@^2.24.0: version "2.24.0" @@ -2782,7 +2782,7 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== -debug@4.2.0, debug@^4.1.0, debug@^4.1.1: +debug@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== @@ -2804,12 +2804,19 @@ debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: ms "2.0.0" debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" +debug@^4.1.0, debug@^4.1.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2965,9 +2972,9 @@ ecc-jsbn@~0.1.1: safer-buffer "^2.1.0" electron-to-chromium@^1.3.591: - version "1.3.598" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.598.tgz#8f757018902ab6190323a8c5f6124d854893a35b" - integrity sha512-G5Ztk23/ubLYVPxPXnB1uu105uzIPd4xB/D8ld8x1GaSC9+vU9NZL16nYZya8H77/7CCKKN7dArzJL3pBs8N7A== + version "1.3.601" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.601.tgz#881824eaef0b2f97c89e1abb5835fdd224997d34" + integrity sha512-ctRyXD9y0mZu8pgeNwBUhLP3Guyr5YuqkfLKYmpTwYx7o9JtCEJme9JVX4xBXPr5ZNvr/iBXUvHLFEVJQThATg== elegant-spinner@^1.0.1: version "1.0.1" @@ -6892,9 +6899,9 @@ synchronous-promise@^2.0.13: integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg== terser@^5.0.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.4.0.tgz#9815c0839072d5c894e22c6fc508fbe9f5e7d7e8" - integrity sha512-3dZunFLbCJis9TAF2VnX+VrQLctRUmt1p3W2kCsJuZE4ZgWqh//+1MZ62EanewrqKoUf4zIaDGZAvml4UDc0OQ== + version "5.5.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.0.tgz#1406fcb4d4bc517add3b22a9694284c040e33448" + integrity sha512-eopt1Gf7/AQyPhpygdKePTzaet31TvQxXvrf7xYUvD/d8qkCJm4SKPDzu+GHK5ZaYTn8rvttfqaZc3swK21e5g== dependencies: commander "^2.20.0" source-map "~0.7.2" @@ -7162,6 +7169,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.1: + version "8.3.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" diff --git a/packages/client/src/old/render/screenRouter.js b/packages/client/src/old/render/screenRouter.js index 0ae02841bf..78dc75b3c4 100644 --- a/packages/client/src/old/render/screenRouter.js +++ b/packages/client/src/old/render/screenRouter.js @@ -43,7 +43,7 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => { return sanitize(url) } - const routes = screens.map(s => makeRootedPath(s.route)) + const routes = screens.map(s => makeRootedPath(s.routing?.route)) let fallback = routes.findIndex(([p]) => p === makeRootedPath("*")) if (fallback < 0) fallback = 0 diff --git a/packages/server/src/api/controllers/accesslevel.js b/packages/server/src/api/controllers/accesslevel.js index 143c633e42..b2985e2953 100644 --- a/packages/server/src/api/controllers/accesslevel.js +++ b/packages/server/src/api/controllers/accesslevel.js @@ -1,10 +1,9 @@ const CouchDB = require("../../db") const { - generateAdminPermissions, - generatePowerUserPermissions, - POWERUSER_LEVEL_ID, - ADMIN_LEVEL_ID, -} = require("../../utilities/accessLevels") + BUILTIN_LEVELS, + AccessLevel, + getAccessLevel, +} = require("../../utilities/security/accessLevels") const { generateAccessLevelID, getAccessLevelParams, @@ -19,85 +18,26 @@ exports.fetch = async function(ctx) { ) const customAccessLevels = body.rows.map(row => row.doc) - const staticAccessLevels = [ - { - _id: ADMIN_LEVEL_ID, - name: "Admin", - permissions: await generateAdminPermissions(ctx.user.appId), - }, - { - _id: POWERUSER_LEVEL_ID, - name: "Power User", - permissions: await generatePowerUserPermissions(ctx.user.appId), - }, - ] - + const staticAccessLevels = [BUILTIN_LEVELS.ADMIN, BUILTIN_LEVELS.POWER] ctx.body = [...staticAccessLevels, ...customAccessLevels] } exports.find = async function(ctx) { - const db = new CouchDB(ctx.user.appId) - ctx.body = await db.get(ctx.params.levelId) + ctx.body = await getAccessLevel(ctx.user.appId, ctx.params.levelId) } -exports.update = async function(ctx) { - const db = new CouchDB(ctx.user.appId) - const level = await db.get(ctx.params.levelId) - level.name = ctx.body.name - level.permissions = ctx.request.body.permissions - const result = await db.put(level) - level._rev = result.rev - ctx.body = level - ctx.message = `Level ${level.name} updated successfully.` -} - -exports.patch = async function(ctx) { - const db = new CouchDB(ctx.user.appId) - const level = await db.get(ctx.params.levelId) - const { removedPermissions, addedPermissions, _rev } = ctx.request.body - - if (!_rev) throw new Error("Must supply a _rev to update an access level") - - level._rev = _rev - - if (removedPermissions) { - level.permissions = level.permissions.filter( - p => - !removedPermissions.some( - rem => rem.name === p.name && rem.itemId === p.itemId - ) - ) - } - - if (addedPermissions) { - level.permissions = [ - ...level.permissions.filter( - p => - !addedPermissions.some( - add => add.name === p.name && add.itemId === p.itemId - ) - ), - ...addedPermissions, - ] - } - - const result = await db.put(level) - level._rev = result.rev - ctx.body = level - ctx.message = `Access Level ${level.name} updated successfully.` -} - -exports.create = async function(ctx) { +exports.save = async function(ctx) { const db = new CouchDB(ctx.user.appId) - const level = { - name: ctx.request.body.name, - _rev: ctx.request.body._rev, - permissions: ctx.request.body.permissions || [], - _id: generateAccessLevelID(), - type: "accesslevel", + let id = ctx.request.body._id || generateAccessLevelID() + const level = new AccessLevel( + id, + ctx.request.body.name, + ctx.request.body.inherits + ) + if (ctx.request.body._rev) { + level._rev = ctx.request.body._rev } - const result = await db.put(level) level._rev = result.rev ctx.body = level diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 2185293352..512299c5c8 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -8,15 +8,18 @@ const fs = require("fs-extra") const { join, resolve } = require("../../utilities/centralPath") const packageJson = require("../../../package.json") const { createLinkView } = require("../../db/linkedRows") +const { createRoutingView } = require("../../utilities/routing") const { downloadTemplate } = require("../../utilities/templates") const { generateAppID, DocumentTypes, SEPARATOR, getPageParams, + getScreenParams, generatePageID, generateScreenID, } = require("../../db/utils") +const { BUILTIN_LEVEL_IDS } = require("../../utilities/security/accessLevels") const { downloadExtractComponentLibraries, } = require("../../utilities/createAppPackage") @@ -26,6 +29,20 @@ const { cloneDeep } = require("lodash/fp") const APP_PREFIX = DocumentTypes.APP + SEPARATOR +// utility function, need to do away with this +async function getMainAndUnauthPage(db) { + let pages = await db.allDocs( + getPageParams(null, { + include_docs: true, + }) + ) + pages = pages.rows.map(row => row.doc) + + const mainPage = pages.find(page => page.name === PageTypes.MAIN) + const unauthPage = pages.find(page => page.name === PageTypes.UNAUTHENTICATED) + return { mainPage, unauthPage } +} + async function createInstance(template) { const appId = generateAppID() @@ -38,6 +55,7 @@ async function createInstance(template) { }) // add view for linked rows await createLinkView(appId) + await createRoutingView(appId) // replicate the template data to the instance DB if (template) { @@ -65,19 +83,36 @@ exports.fetch = async function(ctx) { } } +exports.fetchAppDefinition = async function(ctx) { + const db = new CouchDB(ctx.params.appId) + // TODO: need to get rid of pages here, they shouldn't be needed anymore + const { mainPage, unauthPage } = await getMainAndUnauthPage(db) + const userAccessLevelId = + !ctx.user.accessLevel || !ctx.user.accessLevel._id + ? BUILTIN_LEVEL_IDS.PUBLIC + : ctx.user.accessLevel._id + const correctPage = + userAccessLevelId === BUILTIN_LEVEL_IDS.PUBLIC ? unauthPage : mainPage + const screens = ( + await db.allDocs( + getScreenParams(correctPage._id, { + include_docs: true, + }) + ) + ).rows.map(row => row.doc) + // TODO: need to handle access control here, limit screens to user access level + ctx.body = { + page: correctPage, + screens: screens, + libraries: ["@budibase/standard-components"], + } +} + exports.fetchAppPackage = async function(ctx) { const db = new CouchDB(ctx.params.appId) const application = await db.get(ctx.params.appId) - let pages = await db.allDocs( - getPageParams(null, { - include_docs: true, - }) - ) - pages = pages.rows.map(row => row.doc) - - const mainPage = pages.find(page => page.name === PageTypes.MAIN) - const unauthPage = pages.find(page => page.name === PageTypes.UNAUTHENTICATED) + const { mainPage, unauthPage } = await getMainAndUnauthPage(db) ctx.body = { application, pages: { diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index 2c162587b3..21136b0214 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -34,6 +34,7 @@ exports.authenticate = async ctx => { userId: dbUser._id, accessLevelId: dbUser.accessLevelId, version: app.version, + permissions: dbUser.permissions || [], } // if in cloud add the user api key if (env.CLOUD) { diff --git a/packages/server/src/api/controllers/routing.js b/packages/server/src/api/controllers/routing.js new file mode 100644 index 0000000000..8d96863593 --- /dev/null +++ b/packages/server/src/api/controllers/routing.js @@ -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} 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 +} diff --git a/packages/server/src/api/controllers/screen.js b/packages/server/src/api/controllers/screen.js index 88166bf0b2..694d171fff 100644 --- a/packages/server/src/api/controllers/screen.js +++ b/packages/server/src/api/controllers/screen.js @@ -1,20 +1,28 @@ const CouchDB = require("../../db") const { getScreenParams, generateScreenID } = require("../../db/utils") +const { AccessController } = require("../../utilities/security/accessLevels") exports.fetch = async ctx => { - const db = new CouchDB(ctx.user.appId) + const appId = ctx.user.appId + const db = new CouchDB(appId) - const screens = await db.allDocs( - getScreenParams(null, { - include_docs: true, - }) + const screens = ( + await db.allDocs( + getScreenParams(null, { + include_docs: true, + }) + ) + ).rows.map(element => element.doc) + + ctx.body = await new AccessController(appId).checkScreensAccess( + screens, + ctx.user.accessLevel._id ) - - ctx.body = screens.rows.map(element => element.doc) } exports.find = async ctx => { - const db = new CouchDB(ctx.user.appId) + const appId = ctx.user.appId + const db = new CouchDB(appId) const screens = await db.allDocs( getScreenParams(ctx.params.pageId, { @@ -22,7 +30,10 @@ exports.find = async ctx => { }) ) - ctx.body = screens.response.rows + ctx.body = await new AccessController(appId).checkScreensAccess( + screens, + ctx.user.accessLevel._id + ) } exports.save = async ctx => { diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 5e4f963f5b..c51e1fd2b8 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -2,9 +2,11 @@ const CouchDB = require("../../db") const bcrypt = require("../../utilities/bcrypt") const { generateUserID, getUserParams } = require("../../db/utils") const { - POWERUSER_LEVEL_ID, - ADMIN_LEVEL_ID, -} = require("../../utilities/accessLevels") + BUILTIN_LEVEL_ID_ARRAY, +} = require("../../utilities/security/accessLevels") +const { + BUILTIN_PERMISSION_NAMES, +} = require("../../utilities/security/permissions") exports.fetch = async function(ctx) { const database = new CouchDB(ctx.user.appId) @@ -18,7 +20,13 @@ exports.fetch = async function(ctx) { exports.create = async function(ctx) { const db = new CouchDB(ctx.user.appId) - const { username, password, name, accessLevelId } = ctx.request.body + const { + username, + password, + name, + accessLevelId, + permissions, + } = ctx.request.body if (!username || !password) { ctx.throw(400, "Username and Password Required.") @@ -35,6 +43,7 @@ exports.create = async function(ctx) { name: name || username, type: "user", accessLevelId, + permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER], } try { @@ -89,10 +98,7 @@ exports.find = async function(ctx) { const checkAccessLevel = async (db, accessLevelId) => { if (!accessLevelId) return - if ( - accessLevelId === POWERUSER_LEVEL_ID || - accessLevelId === ADMIN_LEVEL_ID - ) { + if (BUILTIN_LEVEL_ID_ARRAY.indexOf(accessLevelId) !== -1) { return { _id: accessLevelId, name: accessLevelId, diff --git a/packages/server/src/api/controllers/view/index.js b/packages/server/src/api/controllers/view/index.js index 57d4862b7e..0b5b18a93c 100644 --- a/packages/server/src/api/controllers/view/index.js +++ b/packages/server/src/api/controllers/view/index.js @@ -5,6 +5,7 @@ const { join } = require("../../../utilities/centralPath") const os = require("os") const exporters = require("./exporters") const { fetchView } = require("../row") +const { ViewNames } = require("../../../db/utils") const controller = { fetch: async ctx => { @@ -13,8 +14,8 @@ const controller = { const response = [] for (let name of Object.keys(designDoc.views)) { - // Only return custom views - if (name === "by_link") { + // Only return custom views, not built ins + if (Object.values(ViewNames).indexOf(name) !== -1) { continue } response.push({ diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 4a4e80d1ed..c76eb82643 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -4,25 +4,7 @@ const compress = require("koa-compress") const zlib = require("zlib") const { budibaseAppsDir } = require("../utilities/budibaseDir") const { isDev } = require("../utilities") -const { - authRoutes, - pageRoutes, - screenRoutes, - userRoutes, - deployRoutes, - applicationRoutes, - rowRoutes, - tableRoutes, - viewRoutes, - staticRoutes, - componentRoutes, - automationRoutes, - accesslevelRoutes, - apiKeysRoutes, - templatesRoutes, - analyticsRoutes, - webhookRoutes, -} = require("./routes") +const { mainRoutes, authRoutes, staticRoutes } = require("./routes") const router = new Router() const env = require("../environment") @@ -72,52 +54,12 @@ router.use(authRoutes.routes()) router.use(authRoutes.allowedMethods()) // authenticated routes -router.use(viewRoutes.routes()) -router.use(viewRoutes.allowedMethods()) - -router.use(tableRoutes.routes()) -router.use(tableRoutes.allowedMethods()) - -router.use(rowRoutes.routes()) -router.use(rowRoutes.allowedMethods()) - -router.use(userRoutes.routes()) -router.use(userRoutes.allowedMethods()) - -router.use(automationRoutes.routes()) -router.use(automationRoutes.allowedMethods()) - -router.use(webhookRoutes.routes()) -router.use(webhookRoutes.allowedMethods()) - -router.use(deployRoutes.routes()) -router.use(deployRoutes.allowedMethods()) - -router.use(templatesRoutes.routes()) -router.use(templatesRoutes.allowedMethods()) -// end auth routes - -router.use(pageRoutes.routes()) -router.use(pageRoutes.allowedMethods()) - -router.use(screenRoutes.routes()) -router.use(screenRoutes.allowedMethods()) - -router.use(applicationRoutes.routes()) -router.use(applicationRoutes.allowedMethods()) - -router.use(componentRoutes.routes()) -router.use(componentRoutes.allowedMethods()) - -router.use(accesslevelRoutes.routes()) -router.use(accesslevelRoutes.allowedMethods()) - -router.use(apiKeysRoutes.routes()) -router.use(apiKeysRoutes.allowedMethods()) - -router.use(analyticsRoutes.routes()) -router.use(analyticsRoutes.allowedMethods()) +for (let route of mainRoutes) { + router.use(route.routes()) + router.use(route.allowedMethods()) +} +// WARNING - static routes will catch everything else after them this must be last router.use(staticRoutes.routes()) router.use(staticRoutes.allowedMethods()) diff --git a/packages/server/src/api/routes/accesslevel.js b/packages/server/src/api/routes/accesslevel.js index af1ff80ec6..1f21a1ea29 100644 --- a/packages/server/src/api/routes/accesslevel.js +++ b/packages/server/src/api/routes/accesslevel.js @@ -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 diff --git a/packages/server/src/api/routes/analytics.js b/packages/server/src/api/routes/analytics.js index 626e3c2994..0d5e38c34d 100644 --- a/packages/server/src/api/routes/analytics.js +++ b/packages/server/src/api/routes/analytics.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") const controller = require("../controllers/analytics") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/apikeys.js b/packages/server/src/api/routes/apikeys.js index bec9ab677c..d6d0edeac0 100644 --- a/packages/server/src/api/routes/apikeys.js +++ b/packages/server/src/api/routes/apikeys.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/apikeys") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/application.js b/packages/server/src/api/routes/application.js index aeb815d38c..3ee5da058c 100644 --- a/packages/server/src/api/routes/application.js +++ b/packages/server/src/api/routes/application.js @@ -1,11 +1,12 @@ const Router = require("@koa/router") const controller = require("../controllers/application") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() router + .get("/api/:appId/definition", controller.fetchAppDefinition) .get("/api/applications", authorized(BUILDER), controller.fetch) .get( "/api/:appId/appPackage", diff --git a/packages/server/src/api/routes/automation.js b/packages/server/src/api/routes/automation.js index 3ac7937da2..8644c75787 100644 --- a/packages/server/src/api/routes/automation.js +++ b/packages/server/src/api/routes/automation.js @@ -2,7 +2,11 @@ const Router = require("@koa/router") const controller = require("../controllers/automation") const authorized = require("../../middleware/authorized") const joiValidator = require("../../middleware/joi-validator") -const { BUILDER, EXECUTE_AUTOMATION } = require("../../utilities/accessLevels") +const { + BUILDER, + PermissionLevels, + PermissionTypes, +} = require("../../utilities/security/permissions") const Joi = require("joi") const router = Router() @@ -75,7 +79,7 @@ router ) .post( "/api/automations/:id/trigger", - authorized(EXECUTE_AUTOMATION), + authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), controller.trigger ) .delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy) diff --git a/packages/server/src/api/routes/component.js b/packages/server/src/api/routes/component.js index 8fbe7ac41a..e9db3bee76 100644 --- a/packages/server/src/api/routes/component.js +++ b/packages/server/src/api/routes/component.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/component") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/deploy.js b/packages/server/src/api/routes/deploy.js index 4f7aa9b33b..d8667c6fc1 100644 --- a/packages/server/src/api/routes/deploy.js +++ b/packages/server/src/api/routes/deploy.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/deploy") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index a19742097c..44f2d08509 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -15,10 +15,10 @@ const deployRoutes = require("./deploy") const apiKeysRoutes = require("./apikeys") const templatesRoutes = require("./templates") const analyticsRoutes = require("./analytics") +const routingRoutes = require("./routing") -module.exports = { +exports.mainRoutes = [ deployRoutes, - authRoutes, pageRoutes, screenRoutes, userRoutes, @@ -26,7 +26,6 @@ module.exports = { rowRoutes, tableRoutes, viewRoutes, - staticRoutes, componentRoutes, automationRoutes, accesslevelRoutes, @@ -34,4 +33,8 @@ module.exports = { templatesRoutes, analyticsRoutes, webhookRoutes, -} + routingRoutes, +] + +exports.authRoutes = authRoutes +exports.staticRoutes = staticRoutes diff --git a/packages/server/src/api/routes/pages.js b/packages/server/src/api/routes/pages.js index 1ec01dc780..43fb0e764c 100644 --- a/packages/server/src/api/routes/pages.js +++ b/packages/server/src/api/routes/pages.js @@ -1,6 +1,6 @@ const Router = require("@koa/router") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const controller = require("../controllers/page") const router = Router() diff --git a/packages/server/src/api/routes/routing.js b/packages/server/src/api/routes/routing.js new file mode 100644 index 0000000000..32eb14d390 --- /dev/null +++ b/packages/server/src/api/routes/routing.js @@ -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 diff --git a/packages/server/src/api/routes/row.js b/packages/server/src/api/routes/row.js index ae5ae772a6..4409f7f27b 100644 --- a/packages/server/src/api/routes/row.js +++ b/packages/server/src/api/routes/row.js @@ -2,46 +2,49 @@ const Router = require("@koa/router") const rowController = require("../controllers/row") const authorized = require("../../middleware/authorized") const usage = require("../../middleware/usageQuota") -const { READ_TABLE, WRITE_TABLE } = require("../../utilities/accessLevels") +const { + PermissionLevels, + PermissionTypes, +} = require("../../utilities/security/permissions") const router = Router() router .get( "/api/:tableId/:rowId/enrich", - authorized(READ_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.READ), rowController.fetchEnrichedRow ) .get( "/api/:tableId/rows", - authorized(READ_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.READ), rowController.fetchTableRows ) .get( "/api/:tableId/rows/:rowId", - authorized(READ_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.READ), rowController.find ) .post("/api/rows/search", rowController.search) .post( "/api/:tableId/rows", - authorized(WRITE_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), usage, rowController.save ) .patch( "/api/:tableId/rows/:id", - authorized(WRITE_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), rowController.patch ) .post( "/api/:tableId/rows/validate", - authorized(WRITE_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), rowController.validate ) .delete( "/api/:tableId/rows/:rowId/:revId", - authorized(WRITE_TABLE, ctx => ctx.params.tableId), + authorized(PermissionTypes.TABLE, PermissionLevels.WRITE), usage, rowController.destroy ) diff --git a/packages/server/src/api/routes/screen.js b/packages/server/src/api/routes/screen.js index 407bbd1a94..ce49f66043 100644 --- a/packages/server/src/api/routes/screen.js +++ b/packages/server/src/api/routes/screen.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/screen") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const joiValidator = require("../../middleware/joi-validator") const Joi = require("joi") @@ -12,17 +12,20 @@ function generateSaveValidation() { return joiValidator.body(Joi.object({ _css: Joi.string().allow(""), name: Joi.string().required(), - route: Joi.string().required(), + routing: Joi.object({ + route: Joi.string().required(), + accessLevelId: Joi.string().required().allow(""), + }).required().unknown(true), props: Joi.object({ - _id: Joi.string().required(), - _component: Joi.string().required(), - _children: Joi.array().required(), - _instanceName: Joi.string().required(), - _styles: Joi.object().required(), - type: Joi.string().optional(), - table: Joi.string().optional(), - }).required().unknown(true), - }).unknown(true)) + _id: Joi.string().required(), + _component: Joi.string().required(), + _children: Joi.array().required(), + _instanceName: Joi.string().required(), + _styles: Joi.object().required(), + type: Joi.string().optional(), + table: Joi.string().optional(), + }).required().unknown(true), + }).unknown(true)) } router diff --git a/packages/server/src/api/routes/static.js b/packages/server/src/api/routes/static.js index 5c33900eca..a519a63781 100644 --- a/packages/server/src/api/routes/static.js +++ b/packages/server/src/api/routes/static.js @@ -3,7 +3,7 @@ const controller = require("../controllers/static") const { budibaseTempDir } = require("../../utilities/budibaseDir") const env = require("../../environment") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const usage = require("../../middleware/usageQuota") const router = Router() diff --git a/packages/server/src/api/routes/table.js b/packages/server/src/api/routes/table.js index 40bfa9326f..ef0eb7caec 100644 --- a/packages/server/src/api/routes/table.js +++ b/packages/server/src/api/routes/table.js @@ -1,7 +1,11 @@ const Router = require("@koa/router") const tableController = require("../controllers/table") const authorized = require("../../middleware/authorized") -const { BUILDER, READ_TABLE } = require("../../utilities/accessLevels") +const { + BUILDER, + PermissionLevels, + PermissionTypes, +} = require("../../utilities/security/permissions") const router = Router() @@ -9,7 +13,7 @@ router .get("/api/tables", authorized(BUILDER), tableController.fetch) .get( "/api/tables/:id", - authorized(READ_TABLE, ctx => ctx.params.id), + authorized(PermissionTypes.TABLE, PermissionLevels.READ), tableController.find ) .post("/api/tables", authorized(BUILDER), tableController.save) diff --git a/packages/server/src/api/routes/templates.js b/packages/server/src/api/routes/templates.js index 3e481610ce..05882a22ea 100644 --- a/packages/server/src/api/routes/templates.js +++ b/packages/server/src/api/routes/templates.js @@ -1,7 +1,7 @@ const Router = require("@koa/router") const controller = require("../controllers/templates") const authorized = require("../../middleware/authorized") -const { BUILDER } = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/security/permissions") const router = Router() diff --git a/packages/server/src/api/routes/tests/accesslevel.spec.js b/packages/server/src/api/routes/tests/accesslevel.spec.js index 3362cbd713..eeb786f7d3 100644 --- a/packages/server/src/api/routes/tests/accesslevel.spec.js +++ b/packages/server/src/api/routes/tests/accesslevel.spec.js @@ -6,13 +6,10 @@ const { defaultHeaders } = require("./couchTestUtils") const { - generateAdminPermissions, - generatePowerUserPermissions, - POWERUSER_LEVEL_ID, - ADMIN_LEVEL_ID, - READ_TABLE, - WRITE_TABLE, -} = require("../../../utilities/accessLevels") + BUILTIN_LEVEL_IDS, +} = require("../../../utilities/security/accessLevels") + +const accessLevelBody = { name: "user", inherits: BUILTIN_LEVEL_IDS.BASIC } describe("/accesslevels", () => { let server @@ -41,7 +38,7 @@ describe("/accesslevels", () => { it("returns a success message when level is successfully created", async () => { const res = await request .post(`/api/accesslevels`) - .send({ name: "user" }) + .send(accessLevelBody) .set(defaultHeaders(appId)) .expect('Content-Type', /json/) .expect(200) @@ -49,7 +46,6 @@ describe("/accesslevels", () => { expect(res.res.statusMessage).toEqual("Access Level 'user' created successfully.") expect(res.body._id).toBeDefined() expect(res.body._rev).toBeDefined() - expect(res.body.permissions).toEqual([]) }) }); @@ -59,7 +55,7 @@ describe("/accesslevels", () => { it("should list custom levels, plus 2 default levels", async () => { const createRes = await request .post(`/api/accesslevels`) - .send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE }] }) + .send(accessLevelBody) .set(defaultHeaders(appId)) .expect('Content-Type', /json/) .expect(200) @@ -74,16 +70,17 @@ describe("/accesslevels", () => { expect(res.body.length).toBe(3) - const adminLevel = res.body.find(r => r._id === ADMIN_LEVEL_ID) + const adminLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.ADMIN) + expect(adminLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.POWER) expect(adminLevel).toBeDefined() - expect(adminLevel.permissions).toEqual(await generateAdminPermissions(appId)) - const powerUserLevel = res.body.find(r => r._id === POWERUSER_LEVEL_ID) + const powerUserLevel = res.body.find(r => r._id === BUILTIN_LEVEL_IDS.POWER) + expect(powerUserLevel.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC) expect(powerUserLevel).toBeDefined() - expect(powerUserLevel.permissions).toEqual(await generatePowerUserPermissions(appId)) const customLevelFetched = res.body.find(r => r._id === customLevel._id) - expect(customLevelFetched.permissions).toEqual(customLevel.permissions) + expect(customLevelFetched.inherits).toEqual(BUILTIN_LEVEL_IDS.BASIC) + expect(customLevelFetched).toBeDefined() }) }); @@ -92,7 +89,7 @@ describe("/accesslevels", () => { it("should delete custom access level", async () => { const createRes = await request .post(`/api/accesslevels`) - .send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE } ] }) + .send({ name: "user" }) .set(defaultHeaders(appId)) .expect('Content-Type', /json/) .expect(200) @@ -110,71 +107,4 @@ describe("/accesslevels", () => { .expect(404) }) }) - - describe("patch", () => { - it("should add given permissions", async () => { - const createRes = await request - .post(`/api/accesslevels`) - .send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE }] }) - .set(defaultHeaders(appId)) - .expect('Content-Type', /json/) - .expect(200) - - const customLevel = createRes.body - - await request - .patch(`/api/accesslevels/${customLevel._id}`) - .send({ - _rev: customLevel._rev, - addedPermissions: [ { itemId: table._id, name: WRITE_TABLE } ] - }) - .set(defaultHeaders(appId)) - .expect('Content-Type', /json/) - .expect(200) - - const finalRes = await request - .get(`/api/accesslevels/${customLevel._id}`) - .set(defaultHeaders(appId)) - .expect(200) - - expect(finalRes.body.permissions.length).toBe(2) - expect(finalRes.body.permissions.some(p => p.name === WRITE_TABLE)).toBe(true) - expect(finalRes.body.permissions.some(p => p.name === READ_TABLE)).toBe(true) - }) - - it("should remove given permissions", async () => { - const createRes = await request - .post(`/api/accesslevels`) - .send({ - name: "user", - permissions: [ - { itemId: table._id, name: READ_TABLE }, - { itemId: table._id, name: WRITE_TABLE }, - ] - }) - .set(defaultHeaders(appId)) - .expect('Content-Type', /json/) - .expect(200) - - const customLevel = createRes.body - - await request - .patch(`/api/accesslevels/${customLevel._id}`) - .send({ - _rev: customLevel._rev, - removedPermissions: [ { itemId: table._id, name: WRITE_TABLE }] - }) - .set(defaultHeaders(appId)) - .expect('Content-Type', /json/) - .expect(200) - - const finalRes = await request - .get(`/api/accesslevels/${customLevel._id}`) - .set(defaultHeaders(appId)) - .expect(200) - - expect(finalRes.body.permissions.length).toBe(1) - expect(finalRes.body.permissions.some(p => p.name === READ_TABLE)).toBe(true) - }) - }) }); diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js index 02a75d77fd..85f4c44a62 100644 --- a/packages/server/src/api/routes/tests/couchTestUtils.js +++ b/packages/server/src/api/routes/tests/couchTestUtils.js @@ -1,11 +1,11 @@ const CouchDB = require("../../../db") const supertest = require("supertest") const { - POWERUSER_LEVEL_ID, - ANON_LEVEL_ID, - BUILDER_LEVEL_ID, - generateAdminPermissions, -} = require("../../../utilities/accessLevels") + BUILTIN_LEVEL_IDS, +} = require("../../../utilities/security/accessLevels") +const { + BUILTIN_PERMISSION_NAMES, +} = require("../../../utilities/security/permissions") const packageJson = require("../../../../package") const jwt = require("jsonwebtoken") const env = require("../../../environment") @@ -26,7 +26,7 @@ exports.supertest = async () => { exports.defaultHeaders = appId => { const builderUser = { userId: "BUILDER", - accessLevelId: BUILDER_LEVEL_ID, + accessLevelId: BUILTIN_LEVEL_IDS.BUILDER, } const builderToken = jwt.sign(builderUser, env.JWT_SECRET) @@ -126,21 +126,13 @@ exports.createUser = async ( name: "Bill", username, password, - accessLevelId: POWERUSER_LEVEL_ID, + accessLevelId: BUILTIN_LEVEL_IDS.POWER, }) return res.body } -const createUserWithOnePermission = async ( - request, - appId, - permName, - itemId -) => { - let permissions = await generateAdminPermissions(appId) - permissions = permissions.filter( - p => p.name === permName && p.itemId === itemId - ) +const createUserWithOnePermission = async (request, appId, permName) => { + let permissions = [permName] return await createUserWithPermissions( request, @@ -151,7 +143,7 @@ const createUserWithOnePermission = async ( } const createUserWithAdminPermissions = async (request, appId) => { - let permissions = await generateAdminPermissions(appId) + let permissions = [BUILTIN_PERMISSION_NAMES.ADMIN] return await createUserWithPermissions( request, @@ -164,13 +156,9 @@ const createUserWithAdminPermissions = async (request, appId) => { const createUserWithAllPermissionExceptOne = async ( request, appId, - permName, - itemId + permName ) => { - let permissions = await generateAdminPermissions(appId) - permissions = permissions.filter( - p => !(p.name === permName && p.itemId === itemId) - ) + let permissions = [permName] return await createUserWithPermissions( request, @@ -186,11 +174,6 @@ const createUserWithPermissions = async ( permissions, username ) => { - const accessRes = await request - .post(`/api/accesslevels`) - .send({ name: "TestLevel", permissions }) - .set(exports.defaultHeaders(appId)) - const password = `password_${username}` await request .post(`/api/users`) @@ -199,12 +182,13 @@ const createUserWithPermissions = async ( name: username, username, password, - accessLevelId: accessRes.body._id, + accessLevelId: BUILTIN_LEVEL_IDS.POWER, + permissions, }) const anonUser = { userId: "ANON", - accessLevelId: ANON_LEVEL_ID, + accessLevelId: BUILTIN_LEVEL_IDS.PUBLIC, appId: appId, version: packageJson.version, } @@ -232,15 +216,10 @@ exports.testPermissionsForEndpoint = async ({ url, body, appId, - permissionName, - itemId, + permName1, + permName2, }) => { - const headers = await createUserWithOnePermission( - request, - appId, - permissionName, - itemId - ) + const headers = await createUserWithOnePermission(request, appId, permName1) await createRequest(request, method, url, body) .set(headers) @@ -249,8 +228,7 @@ exports.testPermissionsForEndpoint = async ({ const noPermsHeaders = await createUserWithAllPermissionExceptOne( request, appId, - permissionName, - itemId + permName2 ) await createRequest(request, method, url, body) diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index a569902bae..f9277039ea 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -5,11 +5,12 @@ const { createUser, testPermissionsForEndpoint, } = require("./couchTestUtils") -const { - POWERUSER_LEVEL_ID, - LIST_USERS, - USER_MANAGEMENT -} = require("../../../utilities/accessLevels") +const { + BUILTIN_PERMISSION_NAMES, +} = require("../../../utilities/security/permissions") +const { + BUILTIN_LEVEL_IDS, +} = require("../../../utilities/security/accessLevels") describe("/users", () => { let request @@ -53,7 +54,8 @@ describe("/users", () => { method: "GET", url: `/api/users`, appId: appId, - permissionName: LIST_USERS, + permName1: BUILTIN_PERMISSION_NAMES.POWER, + permName2: BUILTIN_PERMISSION_NAMES.WRITE, }) }) @@ -65,7 +67,7 @@ describe("/users", () => { const res = await request .post(`/api/users`) .set(defaultHeaders(appId)) - .send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: POWERUSER_LEVEL_ID }) + .send({ name: "Bill", username: "bill", password: "bills_password", accessLevelId: BUILTIN_LEVEL_IDS.POWER }) .expect(200) .expect('Content-Type', /json/) @@ -77,10 +79,11 @@ describe("/users", () => { await testPermissionsForEndpoint({ request, method: "POST", - body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: POWERUSER_LEVEL_ID }, + body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: BUILTIN_LEVEL_IDS.POWER }, url: `/api/users`, appId: appId, - permissionName: USER_MANAGEMENT, + permName1: BUILTIN_PERMISSION_NAMES.ADMIN, + permName2: BUILTIN_PERMISSION_NAMES.POWER, }) }) diff --git a/packages/server/src/api/routes/user.js b/packages/server/src/api/routes/user.js index 5289439e41..9394d842bd 100644 --- a/packages/server/src/api/routes/user.js +++ b/packages/server/src/api/routes/user.js @@ -1,19 +1,39 @@ const Router = require("@koa/router") const controller = require("../controllers/user") const authorized = require("../../middleware/authorized") -const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels") +const { + PermissionLevels, + PermissionTypes, +} = require("../../utilities/security/permissions") const usage = require("../../middleware/usageQuota") const router = Router() router - .get("/api/users", authorized(LIST_USERS), controller.fetch) - .get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find) - .put("/api/users/", authorized(USER_MANAGEMENT), controller.update) - .post("/api/users", authorized(USER_MANAGEMENT), usage, controller.create) + .get( + "/api/users", + authorized(PermissionTypes.USER, PermissionLevels.READ), + controller.fetch + ) + .get( + "/api/users/:username", + authorized(PermissionTypes.USER, PermissionLevels.READ), + controller.find + ) + .put( + "/api/users/", + authorized(PermissionTypes.USER, PermissionLevels.WRITE), + controller.update + ) + .post( + "/api/users", + authorized(PermissionTypes.USER, PermissionLevels.WRITE), + usage, + controller.create + ) .delete( "/api/users/:username", - authorized(USER_MANAGEMENT), + authorized(PermissionTypes.USER, PermissionLevels.WRITE), usage, controller.destroy ) diff --git a/packages/server/src/api/routes/view.js b/packages/server/src/api/routes/view.js index 3657a9e829..17277b346c 100644 --- a/packages/server/src/api/routes/view.js +++ b/packages/server/src/api/routes/view.js @@ -2,7 +2,11 @@ const Router = require("@koa/router") const viewController = require("../controllers/view") const rowController = require("../controllers/row") const authorized = require("../../middleware/authorized") -const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels") +const { + BUILDER, + PermissionTypes, + PermissionLevels, +} = require("../../utilities/security/permissions") const usage = require("../../middleware/usageQuota") const router = Router() @@ -10,7 +14,7 @@ const router = Router() router .get( "/api/views/:viewName", - authorized(READ_VIEW, ctx => ctx.params.viewName), + authorized(PermissionTypes.VIEW, PermissionLevels.READ), rowController.fetchView ) .get("/api/views", authorized(BUILDER), viewController.fetch) diff --git a/packages/server/src/api/routes/webhook.js b/packages/server/src/api/routes/webhook.js index a7072904ed..fdcf14e490 100644 --- a/packages/server/src/api/routes/webhook.js +++ b/packages/server/src/api/routes/webhook.js @@ -2,7 +2,11 @@ const Router = require("@koa/router") const controller = require("../controllers/webhook") const authorized = require("../../middleware/authorized") const joiValidator = require("../../middleware/joi-validator") -const { BUILDER, EXECUTE_WEBHOOK } = require("../../utilities/accessLevels") +const { + BUILDER, + PermissionTypes, + PermissionLevels, +} = require("../../utilities/security/permissions") const Joi = require("joi") const router = Router() @@ -38,7 +42,7 @@ router ) .post( "/api/webhooks/trigger/:instance/:id", - authorized(EXECUTE_WEBHOOK), + authorized(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE), controller.trigger ) diff --git a/packages/server/src/automations/steps/createUser.js b/packages/server/src/automations/steps/createUser.js index 07d9f05316..4b6250ce36 100644 --- a/packages/server/src/automations/steps/createUser.js +++ b/packages/server/src/automations/steps/createUser.js @@ -1,4 +1,4 @@ -const accessLevels = require("../../utilities/accessLevels") +const accessLevels = require("../../utilities/security/accessLevels") const userController = require("../../api/controllers/user") const env = require("../../environment") const usage = require("../../utilities/usageQuota") @@ -11,7 +11,7 @@ module.exports.definition = { type: "ACTION", stepId: "CREATE_USER", inputs: { - accessLevelId: accessLevels.POWERUSER_LEVEL_ID, + accessLevelId: accessLevels.BUILTIN_LEVEL_IDS.POWER, }, schema: { inputs: { @@ -28,8 +28,8 @@ module.exports.definition = { accessLevelId: { type: "string", title: "Access Level", - enum: accessLevels.ACCESS_LEVELS, - pretty: Object.values(accessLevels.PRETTY_ACCESS_LEVELS), + enum: accessLevels.BUILTIN_LEVEL_IDS, + pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY, }, }, required: ["username", "password", "accessLevelId"], diff --git a/packages/server/src/constants/screens.js b/packages/server/src/constants/screens.js index f9a0fb68dc..5c5a9dfd26 100644 --- a/packages/server/src/constants/screens.js +++ b/packages/server/src/constants/screens.js @@ -1,3 +1,5 @@ +const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels") + exports.HOME_SCREEN = { description: "", url: "", @@ -98,6 +100,9 @@ exports.HOME_SCREEN = { ], _instanceName: "Home", }, - route: "/", + routing: { + route: "/", + accessLevelId: BUILTIN_LEVEL_IDS.BASIC, + }, name: "d834fea2-1b3e-4320-ab34-f9009f5ecc59", } diff --git a/packages/server/src/db/linkedRows/linkUtils.js b/packages/server/src/db/linkedRows/linkUtils.js index dc9d8d3f1e..3a9aff6c33 100644 --- a/packages/server/src/db/linkedRows/linkUtils.js +++ b/packages/server/src/db/linkedRows/linkUtils.js @@ -1,5 +1,6 @@ const CouchDB = require("../index") const Sentry = require("@sentry/node") +const { ViewNames, getQueryIndex } = require("../utils") /** * Only needed so that boolean parameters are being used for includeDocs @@ -40,7 +41,7 @@ exports.createLinkView = async appId => { } designDoc.views = { ...designDoc.views, - by_link: view, + [ViewNames.LINK]: view, } await db.put(designDoc) } @@ -76,7 +77,7 @@ exports.getLinkDocuments = async function({ } params.include_docs = !!includeDocs try { - const response = await db.query("database/by_link", params) + const response = await db.query(getQueryIndex(ViewNames.LINK), params) if (includeDocs) { return response.rows.map(row => row.doc) } else { diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index a213dc9066..4edb27a416 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -17,10 +17,20 @@ const DocumentTypes = { SCREEN: "screen", } +const ViewNames = { + LINK: "by_link", + ROUTING: "screen_routes", +} + +exports.ViewNames = ViewNames exports.DocumentTypes = DocumentTypes exports.SEPARATOR = SEPARATOR exports.UNICODE_MAX = UNICODE_MAX +exports.getQueryIndex = viewName => { + return `database/${viewName}` +} + /** * If creating DB allDocs/query params with only a single top level ID this can be used, this * is usually the case as most of our docs are top level e.g. tables, automations, users and so on. diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js index 529581f362..b30e22f0e1 100644 --- a/packages/server/src/middleware/authenticated.js +++ b/packages/server/src/middleware/authenticated.js @@ -1,12 +1,6 @@ const jwt = require("jsonwebtoken") const STATUS_CODES = require("../utilities/statusCodes") -const accessLevelController = require("../api/controllers/accesslevel") -const { - ADMIN_LEVEL_ID, - POWERUSER_LEVEL_ID, - BUILDER_LEVEL_ID, - ANON_LEVEL_ID, -} = require("../utilities/accessLevels") +const { getAccessLevel } = require("../utilities/security/accessLevels") const env = require("../environment") const { AuthTypes } = require("../constants") const { getAppId, getCookieName, setCookie } = require("../utilities") @@ -65,36 +59,3 @@ module.exports = async (ctx, next) => { await next() } - -/** - * Return the full access level object either from constants - * or the database based on the access level ID passed. - * - * @param {*} appId - appId of the user - * @param {*} accessLevelId - the id of the users access level - */ -const getAccessLevel = async (appId, accessLevelId) => { - if ( - accessLevelId === POWERUSER_LEVEL_ID || - accessLevelId === ADMIN_LEVEL_ID || - accessLevelId === BUILDER_LEVEL_ID || - accessLevelId === ANON_LEVEL_ID - ) { - return { - _id: accessLevelId, - name: accessLevelId, - permissions: [], - } - } - - const findAccessContext = { - params: { - levelId: accessLevelId, - }, - user: { - appId, - }, - } - await accessLevelController.find(findAccessContext) - return findAccessContext.body -} diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index 34758903f3..5f4b78b97e 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -1,17 +1,17 @@ +const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels") const { - adminPermissions, - ADMIN_LEVEL_ID, - POWERUSER_LEVEL_ID, - BUILDER_LEVEL_ID, - BUILDER, -} = require("../utilities/accessLevels") + PermissionTypes, + doesHavePermission, +} = require("../utilities/security/permissions") const env = require("../environment") const { apiKeyTable } = require("../db/dynamoClient") const { AuthTypes } = require("../constants") +const ADMIN_ACCESS = [BUILTIN_LEVEL_IDS.ADMIN, BUILTIN_LEVEL_IDS.BUILDER] + const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|")) -module.exports = (permName, getItemId) => async (ctx, next) => { +module.exports = (permType, permLevel = null) => async (ctx, next) => { // webhooks can pass locally if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) { return next() @@ -37,7 +37,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => { } // don't expose builder endpoints in the cloud - if (env.CLOUD && permName === BUILDER) return + if (env.CLOUD && permType === PermissionTypes.BUILDER) return if (!ctx.auth.authenticated) { ctx.throw(403, "Session not authenticated") @@ -47,41 +47,19 @@ module.exports = (permName, getItemId) => async (ctx, next) => { ctx.throw(403, "User not found") } - if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) { + const accessLevel = ctx.user.accessLevel + const permissions = ctx.user.permissions + if (ADMIN_ACCESS.indexOf(accessLevel._id) !== -1) { return next() } - if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) { - return next() - } - - if (permName === BUILDER) { + if (permType === PermissionTypes.BUILDER) { ctx.throw(403, "Not Authorized") - return } - const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "") - - const thisPermissionId = permissionId({ - name: permName, - itemId: getItemId && getItemId(ctx), - }) - - // power user has everything, except the admin specific perms - if ( - ctx.user.accessLevel._id === POWERUSER_LEVEL_ID && - !adminPermissions.map(permissionId).includes(thisPermissionId) - ) { - return next() + if (!doesHavePermission(permType, permLevel, permissions)) { + ctx.throw(403, "User does not have permission") } - if ( - ctx.user.accessLevel.permissions - .map(permissionId) - .includes(thisPermissionId) - ) { - return next() - } - - ctx.throw(403, "Not Authorized") + return next() } diff --git a/packages/server/src/utilities/accessLevels.js b/packages/server/src/utilities/accessLevels.js deleted file mode 100644 index e38a7cf23f..0000000000 --- a/packages/server/src/utilities/accessLevels.js +++ /dev/null @@ -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 diff --git a/packages/server/src/utilities/builder/setBuilderToken.js b/packages/server/src/utilities/builder/setBuilderToken.js index 8cf6c44379..f3adf079ad 100644 --- a/packages/server/src/utilities/builder/setBuilderToken.js +++ b/packages/server/src/utilities/builder/setBuilderToken.js @@ -1,4 +1,5 @@ -const { BUILDER_LEVEL_ID } = require("../accessLevels") +const { BUILTIN_LEVEL_IDS } = require("../security/accessLevels") +const { BUILTIN_PERMISSION_NAMES } = require("../security/permissions") const env = require("../../environment") const CouchDB = require("../../db") const jwt = require("jsonwebtoken") @@ -9,7 +10,8 @@ const APP_PREFIX = DocumentTypes.APP + SEPARATOR module.exports = async (ctx, appId, version) => { const builderUser = { userId: "BUILDER", - accessLevelId: BUILDER_LEVEL_ID, + accessLevelId: BUILTIN_LEVEL_IDS.BUILDER, + permissions: [BUILTIN_PERMISSION_NAMES.ADMIN], version, } if (env.BUDIBASE_API_KEY) { diff --git a/packages/server/src/utilities/permissions.js b/packages/server/src/utilities/permissions.js deleted file mode 100644 index e1513fd0fa..0000000000 --- a/packages/server/src/utilities/permissions.js +++ /dev/null @@ -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 diff --git a/packages/server/src/utilities/routing/index.js b/packages/server/src/utilities/routing/index.js new file mode 100644 index 0000000000..bb0fe5bb62 --- /dev/null +++ b/packages/server/src/utilities/routing/index.js @@ -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 diff --git a/packages/server/src/utilities/routing/routingUtils.js b/packages/server/src/utilities/routing/routingUtils.js new file mode 100644 index 0000000000..5f6a6b5312 --- /dev/null +++ b/packages/server/src/utilities/routing/routingUtils.js @@ -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) +} diff --git a/packages/server/src/utilities/security/accessLevels.js b/packages/server/src/utilities/security/accessLevels.js new file mode 100644 index 0000000000..578cd1e803 --- /dev/null +++ b/packages/server/src/utilities/security/accessLevels.js @@ -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} 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} 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 diff --git a/packages/server/src/utilities/security/permissions.js b/packages/server/src/utilities/security/permissions.js new file mode 100644 index 0000000000..d19f31e393 --- /dev/null +++ b/packages/server/src/utilities/security/permissions.js @@ -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